unstable_cache vs React.cache en Next.js: la query duplicada
Tenía cache configurado y aun así mi base de datos se ejecutó dos veces en el mismo request. El motivo no es un bug, es una confusión muy común entre dos APIs.
Tenía unstable_cache envolviendo mi función de acceso a datos. Debería haberse ejecutado una sola vez por página. Pero al abrir la terminal en desarrollo, vi mi log de “query real a la base de datos” dos veces seguidas, en el mismo request, para el mismo producto.
Mi primer sospechoso fue el cache: “no está funcionando”. Estaba equivocado — el cache sí funcionaba. El problema era otro, y es uno de los matices menos documentados de trabajar con datos en el App Router de Next.js.
Si ya leíste mi artículo sobre los 3 mecanismos de cache en Next.js, esto es la otra cara de la moneda: no es sobre cache entre requests, es sobre lo que pasa dentro de un mismo request.
El escenario exacto donde aparece el bug
Una página de producto típica necesita el mismo dato en dos lugares distintos del árbol de componentes:
// generateMetadata necesita el producto para el <title> y meta description
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const product = await getProductBySlug(slug)
return { title: `${product?.name} | Tienda` }
}
// El componente de la página también lo necesita, para renderizarlo
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const product = await getProductBySlug(slug)
// ...
}
Ambas funciones llaman a getProductBySlug(slug). Next.js las ejecuta en paralelo (o casi), y aquí está la clave: ninguna terminó de escribir en el cache antes de que la otra empezara. Las dos ven un “cache miss” al mismo tiempo, y las dos golpean la base de datos.
Por qué unstable_cache no evita esto (y no es su culpa)
Mucha gente asume que unstable_cache funciona como la memoización automática de fetch() nativo en Next.js — donde llamar el mismo fetch() dos veces en el mismo render solo dispara una petición real de red. Pero esa deduplicación automática es una característica específica de fetch(), no una propiedad general de “cualquier cosa envuelta en cache”.
unstable_cache te da algo distinto y valioso — persistencia entre requests (minutos, horas, hasta que algo lo invalide) — pero no te da deduplicación dentro de un mismo request. Son dos problemas diferentes que se resuelven con herramientas diferentes.
La solución: combinar las dos, cada una resuelve lo suyo
Aquí es donde entra cache() de React (sí, del paquete react, no de next/cache) — una API pensada exactamente para esto: memoizar el resultado de una función durante la vida de un solo request.
import { cache } from 'react'
import { unstable_cache } from 'next/cache'
import { prisma } from '@/lib/prisma'
export const getProductBySlug = cache(
unstable_cache(
async (slug: string) => {
console.log(`🔴 QUERY REAL: getProductBySlug(${slug})`)
return prisma.product.findUnique({ where: { slug } })
},
['product-by-slug'],
{ tags: ['products'], revalidate: 3600 }
)
)
El orden importa, y vale la pena que lo entiendas en vez de memorizarlo:
cache()(por fuera): la primera vez que se llama con unslugdado, en este request, ejecuta la función interna. La segunda llamada con el mismoslug, en el mismo request, devuelve el resultado ya obtenido sin ejecutar nada de nuevo.unstable_cache()(por dentro): se encarga de que, entre requests distintos (o distintos usuarios), el resultado persista según elrevalidate/tagsque configuraste.
Con esto, generateMetadata y ProductPage siguen llamando a la misma función — pero ahora la segunda llamada, dentro del mismo request, ni siquiera llega a unstable_cache, se resuelve directo en la capa de React.cache().
Compruébalo tú mismo
Con el código de arriba, entra a una página de producto y mira tu terminal: el log 🔴 debería aparecer una sola vez, no dos, aunque tanto generateMetadata como el componente de página pidan el mismo producto.
Si quitas el cache() de React y dejas solo unstable_cache, vas a ver el log duplicarse de nuevo en cada primera visita (cuando el Data Cache todavía está “frío”) — la prueba directa de que son dos capas resolviendo dos problemas distintos, no una redundante con la otra.
El matiz que casi nadie explica: React.cache() con datos personales
Hay un caso donde no quieres unstable_cache, pero sí React.cache() a secas: datos específicos de un usuario, como un carrito de compras identificado por cookie.
import { cache } from 'react'
import { cookies } from 'next/headers'
import { prisma } from '@/lib/prisma'
export const getCart = cache(async () => {
const cartId = (await cookies()).get('cartId')?.value
if (!cartId) return null
return prisma.cart.findUnique({
where: { id: cartId },
include: { items: { include: { product: true } } },
})
})
Aquí no hay unstable_cache envolviendo nada. Es intencional: unstable_cache guarda su resultado en el Data Cache compartido del servidor, sin distinguir de qué usuario vino la petición a menos que lo metas explícitamente en la key. Si cacheas ahí un carrito, corres el riesgo real de que el carrito del Usuario A se filtre al Usuario B en la siguiente visita de otra persona. React.cache(), en cambio, solo vive durante ese único request — perfecto para deduplicar sin arriesgar una fuga de datos entre usuarios.
Reglas prácticas para no confundirlas
- ¿El dato es igual para todos los usuarios? (catálogo, contenido público) →
React.cache(unstable_cache(...)), las dos juntas. - ¿El dato es específico de este usuario/sesión? (carrito, perfil) → solo
React.cache(), nuncaunstable_cache. - ¿Ves logs duplicados en el mismo request? Es casi siempre falta de
React.cache()por fuera, no un problema deunstable_cache. - El orden de anidación importa:
cache()por fuera,unstable_cache()por dentro — no al revés.
Conclusión
unstable_cache y React.cache() no compiten entre sí, resuelven problemas en capas distintas del ciclo de vida de un request: uno persiste entre visitas, el otro deduplica dentro de una sola. Confundirlos produce exactamente el síntoma con el que arrancó este artículo — cache “activado” que aun así deja pasar queries duplicadas.
¿Tu aplicación Next.js está haciendo más queries de las que debería sin que lo hayas notado? Agenda una auditoría técnica gratuita de 15 minutos y lo revisamos juntos.
¿Tienes un proyecto en mente?
Convierte tu idea en un producto real
Desarrollo web, aplicaciones a medida y consultoría tecnológica para empresas y startups. Cuéntame tu proyecto y te respondo en menos de 24 horas.