Frontend nextjs

El cache leak silencioso: por qué el carrito no usa unstable_cache

Cachear datos personales en la capa equivocada puede filtrar el carrito de un usuario a otro. Así se distingue qué sí y qué no debe pasar por unstable_cache.

7 min
Next.js Caching Seguridad Arquitectura

Cachear el catálogo de productos con unstable_cache fue una decisión fácil — el mismo dato, para todos los usuarios, con alta reutilización. El carrito de compras es tentador cachear de la misma forma “para ganar performance también ahí”. Es exactamente la decisión que puede terminar mostrando el carrito del Usuario A al Usuario B.

Este artículo explica por qué, con el criterio exacto para decidir qué datos pueden pasar por unstable_cache y cuáles nunca deben hacerlo — algo que conecta directo con lo que ya vimos en unstable_cache vs React.cache.

Dónde vive realmente el Data Cache

unstable_cache no guarda su resultado en la sesión de un usuario específico — lo guarda en un almacén compartido del lado del servidor, indexado por la key que le des. Si dos usuarios distintos disparan la misma función cacheada, con la misma key, el segundo usuario recibe el resultado que quedó guardado por el primero.

Para el catálogo, esto es exactamente lo que quieres: cualquier usuario que visite la home debe ver el mismo listado de productos. Pero para el carrito, es un desastre si no se maneja con cuidado.

El error que parece inocente

// ❌ Peligroso: cachear el carrito con una key genérica
export const getCart = unstable_cache(
  async (cartId: string) => {
    return prisma.cart.findUnique({
      where: { id: cartId },
      include: { items: { include: { product: true } } },
    })
  },
  ['cart'], // la key base NO incluye el cartId de forma explícita en el diseño
  { revalidate: 60 }
)

Aunque unstable_cache sí incorpora los argumentos de la función (cartId, en este caso) en la key final de forma automática — por lo que técnicamente cada cartId distinto genera una entrada distinta — el riesgo real está en otro lugar más sutil: si mañana alguien refactoriza esta función y olvida pasar el cartId como argumento explícito, o lo obtiene de una fuente ambigua (una variable de módulo, un valor por defecto), la protección desaparece silenciosamente, sin ningún error visible en desarrollo.

Por qué React.cache() es la opción correcta para datos por-usuario

// ✅ src/data/cart.ts
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.upsert({
    where: { id: cartId },
    update: {},
    create: { id: cartId },
    include: { items: { include: { product: true } } },
  })
})

Nota lo que no está aquí: nada de unstable_cache. React.cache() a secas memoiza el resultado solo durante la vida de un request — nunca persiste entre requests, y mucho menos entre usuarios distintos. Cada request nuevo, de cualquier usuario, ejecuta la función de cero, lee la cookie de ese request específico, y trae el carrito correspondiente.

Esto elimina por completo la superficie de riesgo: no hay una key compartida que pueda, por error de diseño futuro, filtrar datos entre sesiones — porque no hay persistencia entre requests en absoluto.

El criterio general, no solo para carritos

La pregunta que determina la capa correcta:

PreguntaRespuesta → Capa
¿El resultado es idéntico para cualquier usuario que lo pida?Sí → unstable_cache (+ React.cache para dedup)
¿El resultado depende de quién hace la petición (cookie, sesión, usuario autenticado)?Sí → solo React.cache(), nunca unstable_cache
¿El dato cambia con tanta frecuencia que cachearlo no aporta nada?Considera no cachear en absoluto, como en las búsquedas

Esto no aplica solo a carritos — perfiles de usuario, notificaciones, cualquier dashboard personalizado, historial de pedidos de “mi cuenta”… todos comparten la misma característica: son personales, y por lo tanto no deberían pasar por un cache compartido entre usuarios sin una segmentación explícita y muy cuidadosamente verificada.

Si de verdad necesitas cachear algo personal, hazlo explícito y verificable

Hay casos legítimos donde sí quieres persistencia entre requests para datos por-usuario (por ejemplo, un dashboard costoso de calcular, que no cambia a cada segundo). Si llegas a ese punto, la key debe incluir el identificador del usuario de forma explícita y visible en el código, nunca implícita:

// Si de verdad necesitas persistencia entre requests para datos por-usuario:
export async function getExpensiveUserDashboard(userId: string) {
  return unstable_cache(
    async () => computeExpensiveDashboard(userId),
    [`dashboard-${userId}`], // el userId está EXPLÍCITO en la key, visible a simple vista
    { revalidate: 300 }
  )()
}

Aun así, este patrón exige disciplina de equipo: cualquier revisión de código sobre esta función debería preguntarse específicamente “¿esta key realmente aísla por usuario, o hay algún camino donde eso se pierda?”.

Conclusión

El Data Cache de Next.js no distingue automáticamente entre “dato público” y “dato personal” — esa distinción es una responsabilidad de diseño que recae completamente en quien escribe la función de acceso a datos. La regla simple que evita el riesgo: si el resultado depende de quién pregunta, React.cache() solo, nunca unstable_cache. Es una de esas decisiones que no genera ningún error visible cuando está mal — solo un bug de privacidad esperando el momento equivocado para aparecer.

¿Tu aplicación cachea datos que deberían ser exclusivos de cada usuario? 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.

Solicitar presupuesto Ver LinkedIn