¿Cómo funciona el caché de CDN/edge, y qué encabezados HTTP lo controlan?
Las CDN acercan las respuestas a servidores cerca del usuario, pero solo los encabezados de la respuesta deciden si pueden hacerlo, por cuánto tiempo y para quién.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo funciona el caché de CDN/edge, y qué encabezados HTTP lo controlan?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Qué hace realmente una CDN con una respuesta
Una CDN (o cualquier caché "de borde": un proxy inverso entre tus usuarios y tus servidores de origen) guarda copias de las respuestas en puntos de presencia (PoPs) geográficamente cercanos a los usuarios, para que una petición desde Tokio no tenga que viajar de ida y vuelta hasta un servidor de origen en Virginia. El mecanismo es caché HTTP genérico, no magia: los encabezados de respuesta del origen le dicen a cada caché corriente abajo —la CDN, los proxies corporativos y el propio navegador— si puede almacenar la respuesta, por cuánto tiempo, y bajo qué condiciones debe verificar con el origen antes de reutilizarla.
Ante un miss, el PoP de borde consulta al origen, guarda una copia según los encabezados de la respuesta, y la sirve. Ante un hit, nada llega siquiera a tu origen, que es todo el punto: tanto la carga del origen como la latencia bajan para todo lo que sea cacheable.
#Los encabezados que realmente hacen el trabajo
Cache-Control es el encabezado más importante. Sus directivas se combinan:
| Directiva | Significado |
|---|---|
public | Cualquier caché (navegador, CDN, proxy) puede almacenar esta respuesta |
private | Solo el navegador del usuario final puede cachearla; las CDN y proxies compartidos no deben hacerlo |
no-store | Nunca cachear esto en ningún lugar, ni siquiera brevemente (tokens de autenticación, páginas de pago) |
no-cache | Se puede cachear, pero debe revalidarse con el origen antes de cada reuso |
max-age=N | Fresca durante N segundos para cualquier caché, incluido el navegador |
s-maxage=N | Fresca durante N segundos específicamente en cachés compartidas (CDN); sobrescribe a max-age para ellas, permitiéndote cachear más tiempo en el borde que en el navegador |
must-revalidate | Una vez obsoleta, no debe servirse sin revalidar, incluso si el origen no responde |
stale-while-revalidate=N | Puede servir la copia obsoleta hasta N segundos más mientras se busca una fresca en segundo plano |
stale-if-error=N | Puede seguir sirviendo la copia obsoleta durante N segundos si el origen falla o hace timeout |
immutable | El contenido de esta URL exacta nunca cambiará: se salta la revalidación por completo, incluso en un refresco forzado |
Algunos encabezados complementarios redondean el panorama: ETag es una huella opaca del cuerpo de la respuesta; el cliente la reenvía como If-None-Match en la siguiente petición, y el origen retorna un 304 Not Modified sin cuerpo si nada cambió, así el cliente reutiliza su copia guardada sin volver a descargarla. Vary le dice a las cachés qué encabezados de la petición dividen la clave de caché: Vary: Accept-Encoding significa que las versiones gzip y brotli de una respuesta se cachean por separado; Vary: Accept-Language hace lo mismo por idioma. Si te equivocas con Vary (lo olvidas, o lo sobre-especificas), terminas sirviendo el idioma equivocado a alguien, o destruyes tu tasa de aciertos al fragmentar la caché por cada cadena de user-agent distinta. Age es un encabezado de respuesta que la propia CDN agrega, indicando cuántos segundos atrás el origen generó esa copia cacheada específica: invaluable para depurar "por qué esto está obsoleto".
#Una configuración real
// Express: assets estáticos de build con hash — seguros de cachear para siempreapp.use("/static", express.static("build/static", {setHeaders: (res) => {res.setHeader("Cache-Control", "public, max-age=31536000, immutable");},}));// Una respuesta de API que puede estar hasta 60s obsoleta en el borde,// y puede servir una copia obsoleta 5 minutos más mientras se refresca en segundo planoapp.get("/api/articles/:id", async (req, res) => {const articulo = await getArticle(req.params.id);res.set("Cache-Control", "public, s-maxage=60, stale-while-revalidate=300");res.set("Vary", "Accept-Encoding");res.json(articulo);});// El dashboard de un usuario autenticado — nunca debe cachearlo una caché compartidaapp.get("/api/me/dashboard", requireAuth, async (req, res) => {const datos = await getDashboard(req.user.id);res.set("Cache-Control", "private, no-store");res.json(datos);});
En la capa de la CDN, la configuración equivalente de caché de proxy inverso en Nginx se ve así:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m max_size=1g;server {location /api/articles/ {proxy_pass http://origin_upstream;proxy_cache api_cache;proxy_cache_valid 200 60s;proxy_cache_use_stale error timeout updating http_500 http_502 http_503;proxy_cache_background_update on;add_header X-Cache-Status $upstream_cache_status;}}
proxy_cache_use_stale y proxy_cache_background_update son la versión de Nginx de stale-if-error y stale-while-revalidate: sirve lo que tienes, refresca en silencio, y solo hace esperar al usuario cuando de verdad no hay nada cacheado todavía.
#Purgar es un problema en sí mismo
A diferencia de una única instancia de Redis, la caché de una CDN está replicada en decenas o cientos de PoPs. Purgar "esta URL" tras actualizar el contenido significa avisarle a cada PoP, y esa propagación no es instantánea: la mayoría de las CDN anuncian propagación de purga en el orden de segundos, no de milisegundos. Como purgar por URL exacta no escala cuando un cambio subyacente afecta a muchas páginas cacheadas (un componente de navegación compartido, por ejemplo), las CDN soportan surrogate keys o cache tags: etiquetas cada respuesta cacheada con los ids de todo lo que contribuyó a generarla (Surrogate-Key: article-42 author-7), y una sola llamada de purga para author-7 invalida cada página cacheada que incluyó a ese autor, sin que tengas que rastrear URLs en absoluto.
La habilidad práctica que se evalúa aquí no es memorizar nombres de encabezados: es razonar sobre qué caché, en qué capa, tiene permitido guardar qué respuesta, y por cuánto tiempo, dado quién más podría pedir esa misma URL después.