Phil Karlton dijo que existen dos problemas difíciles en la computación: naming, cache invalidation y off-by-one errors. El chiste es viejo porque el problema de invalidación es real. Pero eso no significa que debas evitar el cache. Significa que necesitas una estrategia.
Los tres niveles de cache web
Una aplicación web típica tiene tres puntos donde el cache ayuda:
- CDN (edge cache): cache estático más cerca del usuario. Imágenes, CSS, JS bundle, páginas pre-renderizadas.
- Application cache: cache en memoria o Redis para datos que cambian poco. Sesiones, configuraciones, queries frecuentes.
- Database cache: query results, prepared statements, materialized views.
Cada nivel tiene características distintas de invalidación. La CDN es la más agresiva (TTL largo, invalidación manual). El application cache es el más flexible (TTL corto, invalidación por evento). El database cache es el más conservador (invalidación automática vía triggers o materialized views).
Redis: el workhorse del application cache
Redis es rápido porque mantiene los datos en memoria. Es la elección correcta para datos que necesitan leerse miles de veces por segundo.
// Cache-aside pattern
async function getUser(id: string): Promise<User> {
const cacheKey = `user:${id}`
// 1. Intenta cache
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
// 2. Cache miss: busca en la base de datos
const user = await db.user.findUnique({ where: { id } })
if (!user) throw new AppError('NOT_FOUND', 404, 'Usuario no encontrado')
// 3. Almacena en cache con TTL
await redis.setex(cacheKey, 3600, JSON.stringify(user)) // 1 hora
return user
}
El patrón cache-aside es el más común: el código intenta cache primero, busca en la base si hay miss, y almacena en cache para la próxima lectura. Es simple y funciona para la mayoría de los casos.
Estrategias de invalidation
Los patrones de invalidación que funcionan en producción:
TTL (Time-To-Live)
El cache expira automáticamente después de un período. Simple, predecible, acepta datos eventualmente consistentes.
// Datos que cambian raramente: TTL largo
await redis.setex('config:app', 86400, JSON.stringify(config)) // 24h
// Datos que cambian frecuentemente: TTL corto
await redis.setex('feed:popular', 300, JSON.stringify(feed)) // 5min
Event-driven invalidation
Cuando el dato cambia, invalida el cache explícitamente. Requiere un evento de dominio que dispare la invalidación.
// Al actualizar usuario, invalida el cache
async function updateUser(id: string, data: UpdateUserDto): Promise<User> {
const user = await db.user.update({ where: { id }, data: data })
// Invalida cache del usuario y listas que lo incluyen
await redis.del(`user:${id}`)
await redis.del('users:active') // invalida cache del listado
return user
}
Write-through cache
Actualiza cache y base de datos en la misma operación. Garantiza consistencia inmediata, pero añade latencia en la escritura.
CDN cache: los headers que importan
Controlar el cache de CDN requiere los headers correctos:
// Cache estático: 1 año, immutable
Cache-Control: public, max-age=31536000, immutable
// API con datos que cambian
Cache-Control: private, max-age=0, must-revalidate
ETag: "abc123"
// Página que cambia por día
Cache-Control: public, max-age=3600, stale-while-revalidate=86400
stale-while-revalidate permite servir contenido stale mientras se revalida en background. El usuario recibe respuesta rápida, y el contenido se actualiza sin delay perceptible.
Cache warming
Después de un deploy o restart, los caches quedan vacíos. Esto causa un pico de latencia (cold start). El cache warming precarga datos frecuentes antes de que llegue el tráfico:
// En el startup del servidor
async function warmCache() {
const popularProducts = await db.product.findMany({
orderBy: { views: 'desc' },
take: 100
})
await Promise.all(popularProducts.map(p =>
redis.setex(`product:${p.id}`, 7200, JSON.stringify(p))
))
}
Cuándo no cachear
No todo necesita cache. Datos que cambian en cada request (saldos en tiempo real), datos sensibles sin control de acceso adecuado, y resultados de queries simples y rápidas no se benefician del cache. El overhead de invalidar y mantener el cache puede ser mayor que la ganancia de performance.
¿Te gustó el contenido?
Construyo productos web y soluciones con IA de la manera correcta — arquitectura sólida, código sostenible y entrega real.
Hablemos