️ Sistema de Caché
Dos capas de defensa para proteger Base de Datos Primaria de picos de tráfico: caché en memoria dentro del proceso Node.js de Plataforma de Despliegue, y caché HTTP en el CDN de Plataforma de Despliegue. Cada capa tiene su propósito.
Arquitectura: dos capas
Capa 1: Plataforma de Despliegue Edge CDN
Capa 2: In-Memory Cache (cache.ts)
Base de Datos Primaria DB (Base de Datos SQL en el edge)
Anti-stampede: el problema y la solución
Cuando el caché expira y 100 usuarios llegan al mismo tiempo, sin protección todos harían 100 queries simultáneas a Base de Datos Primaria. Esto se llama cache stampede o thundering herd.
Sin anti-stampede
- Cache expira
- 100 usuarios llegan
- 100 queries a Base de Datos Primaria simultáneas
- Base de Datos Primaria se satura → errores 500
Con Promise deduplication
- Cache expira
- 100 usuarios llegan
- Usuario 1 inicia fetch → guarda Promise pending
- Usuarios 2–100 reciben la misma Promise
- Solo 1 query llega a Base de Datos Primaria
// Extracto de cache.ts — el corazón del anti-stampede
const pending = new Map<string, Promise<unknown>>();
if (pending.has(key)) {
return pending.get(key); // Reutiliza la Promise existente
}
const promise = fetcher().then((data) => {
store.set(key, { data, expiresAt: now + ttl * 1000 });
pending.delete(key);
return data;
});
pending.set(key, promise);
return promise;API de cache.ts
cached() — obtener o regenerar
import { cached } from '../../lib/cache';
const data = await cached(
'home-posts-v1', // Clave única (incluir versión para cache-busting)
60, // TTL en segundos
async () => { // Función que obtiene datos frescos
return await db.select().from(Post).limit(10);
}
);invalidate() — borrar una clave
import { invalidate } from '../../lib/cache';
// Al publicar o editar un post:
invalidate('home-posts-v1');
invalidate(`post-${slug}-v1`);invalidatePrefix() — borrar todas las claves con un prefijo
import { invalidatePrefix } from '../../lib/cache';
// Al editar cualquier post, borrar todo el caché de blog:
invalidatePrefix('blog-');
invalidatePrefix('post-'); // Borra post-slug1, post-slug2, etc.TTLs por tipo de dato
| Endpoint / Dato | TTL In-Memory | s-maxage CDN | stale-while-revalidate |
|---|---|---|---|
| /api/trending | 600s (10 min) | 600s | 3600s (1h) |
| Posts en home | 60s | 60s | 360s |
| Post individual por slug | 60s | 60s | 360s |
| Lista de categorías | 300s | 300s | 1800s |
| Datos de anime embed (AniList) | 86400s (24h) | — | — |
| HTML generado de embed | 86400s (24h) | — | — |
| Datos de cine (Cinemeta) | 86400s (24h) | — | — |
| Página "Nosotros" | — | 86400s | 604800s (1 semana) |
stale-while-revalidate: el CDN sirve la copia expirada mientras regenera en background. El usuario nunca espera.
Versionado de claves para cache-busting
Al cambiar el diseño de un embed o el formato de los datos, la clave en caché puede tener datos del formato viejo. La solución es versionar la clave:
// Versión vieja — datos del diseño anterior
cached('anime-embed-html-v8-' + animeId, ...)
// Versión nueva — fuerza regeneración para todos los items
cached('anime-embed-html-v9-' + animeId, ...)Plantilla de endpoint cacheado
import type { APIRoute } from 'astro';
import { cached } from '../../lib/cache';
const CACHE_TTL = 60; // segundos
async function fetchData() {
// Tu query a Base de Datos Primaria aquí
const posts = await db.select().from(Post).limit(10);
return posts;
}
export const GET: APIRoute = async () => {
const data = await cached('mi-endpoint-v1', CACHE_TTL, fetchData);
return new Response(JSON.stringify(data), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': `public, s-maxage=${CACHE_TTL}, stale-while-revalidate=${CACHE_TTL * 6}`,
},
});
};Diagnóstico
Post editado no aparece en producción
Causa: La capa CDN no expiró aún.
Embed muestra diseño antiguo
Causa: Clave de caché sin incrementar versión.
Base de Datos Primaria recibe miles de queries
Causa: cached() se llama con claves distintas cada vez.