wandres.dev
CACHÉ HTTP · Las cabeceras que importan

Cache-Control directiva a directiva

Qué hace exactamente cada directiva, cuáles se confunden entre sí, cuáles solo afectan a cachés compartidas, y las combinaciones que sirven para cada tipo de recurso.

⏱ 18 min

Cache-Control tiene una docena de directivas, dos de ellas con nombres tan parecidos que se confunden constantemente y hacen cosas casi opuestas. Repasarlas una a una con precisión es la diferencia entre una política de caché que funciona y una que parece funcionar hasta que alguien no ve un despliegue.

🎯 Al terminar esta lección sabrás
  • Enunciar el efecto exacto de cada directiva de Cache-Control.
  • Distinguir no-cache de no-store y private de no-store.
  • Aplicar las directivas que solo afectan a cachés compartidas.
  • Elegir la combinación correcta para cada tipo de recurso.

Las directivas, una a una

Las directivas de almacenamiento

no-store. Prohíbe almacenar la respuesta en cualquier caché. Es la única directiva que de verdad significa “no guardes esto”. Se usa para datos sensibles que no deben quedar en disco.

no-cache. No significa lo que su nombre sugiere. La respuesta sí se almacena; lo que exige es que se revalide con el servidor antes de cada uso. Es decir, siempre habrá una petición condicional, pero si el servidor responde 304 te ahorras la transferencia.

La confusión entre ambas es la más frecuente del tema, y tiene coste real en las dos direcciones. Poner no-store donde querías no-cache desperdicia la posibilidad de ahorrar transferencia. Poner no-cache donde querías no-store deja datos sensibles en el disco del usuario.

Quieres Directiva
Que no quede rastro en ninguna caché no-store
Que se guarde pero se compruebe siempre antes de usar no-cache

private. La respuesta solo puede almacenarse en la caché del usuario, nunca en una compartida. Es lo que hay que poner en cualquier respuesta que dependa de la sesión.

public. Autoriza explícitamente a las cachés compartidas a almacenar la respuesta, incluso en casos en que por defecto no lo harían, como una respuesta a una petición con cabecera de autorización. Ojo: es una autorización explícita, y usarla sin pensar es exactamente el fallo que filtra datos entre usuarios.

Las directivas de frescura

max-age=N. La respuesta es fresca durante N segundos desde que se generó. Es la directiva principal.

s-maxage=N. Igual, pero solo para cachés compartidas, y tiene precedencia sobre max-age en ellas. Permite políticas distintas para el navegador y para la CDN, que es una de las técnicas más útiles del capítulo:

Cache-Control: public, max-age=60, s-maxage=86400

Eso dice: el navegador guarda esto un minuto; la CDN, un día. Combinado con purga desde la CDN cuando el contenido cambia, da lo mejor de los dos mundos: el usuario recibe contenido casi siempre desde el borde, y tú puedes actualizarlo en segundos sin esperar a que expiren millones de cachés de navegador.

must-revalidate. Una vez rancia, la respuesta no puede servirse sin revalidar con éxito. Sin esta directiva, una caché puede servir contenido rancio en circunstancias como una desconexión de red. Con ella, es preferible el error.

proxy-revalidate. Igual, pero solo aplicable a cachés compartidas.

immutable. Indica que el cuerpo de la respuesta no va a cambiar nunca mientras siga fresco. Su efecto principal es evitar la revalidación en un caso concreto: cuando el usuario recarga la página. Sin ella, una recarga suele disparar peticiones condicionales para todos los subrecursos, aunque estén frescos. Con ella, no. Lo desarrollamos en la lección siguiente.

Las directivas de tolerancia

Dos directivas que permiten servir contenido rancio de forma controlada, y que son de las más rentables del capítulo:

stale-while-revalidate=N. Durante N segundos después de que la respuesta se vuelva rancia, la caché puede servirla inmediatamente y revalidar en segundo plano. El usuario nunca espera.

stale-if-error=N. Durante N segundos después de volverse rancia, si la revalidación falla por un error de red o del servidor, la caché puede servir la copia rancia en lugar de propagar el error.

Las directivas menores

no-transform. Prohíbe a los intermediarios modificar el cuerpo. Sirve contra proxies que recomprimen imágenes o reescriben HTML, algo que hacían algunos operadores móviles.

must-understand. Indica que la respuesta solo debe almacenarse si la caché entiende los requisitos de cacheabilidad basados en el código de estado. Se acompaña siempre de no-store como respaldo para cachés que no la entiendan.

Las combinaciones que se usan de verdad

Esta tabla es lo que hay que tener a mano. Cada fila es una política completa.

Tipo de recurso Cabecera
Estático con hash en el nombre Cache-Control: public, max-age=31536000, immutable
HTML de una página pública Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=86400
HTML de una página personalizada Cache-Control: private, no-cache
Respuesta de API pública que cambia poco Cache-Control: public, max-age=60, stale-while-revalidate=600, stale-if-error=86400
Respuesta de API con datos del usuario Cache-Control: private, no-cache
Datos sensibles, por ejemplo un extracto bancario Cache-Control: no-store
Imagen de usuario que puede cambiar Cache-Control: private, max-age=3600
Archivo que se descarga una vez Cache-Control: public, max-age=86400

El valor 31536000 son 365 días en segundos, que es el máximo que la especificación recomienda no superar. Es el valor canónico para recursos con hash en el nombre.

La segunda fila merece explicación porque es la más sofisticada. max-age=0 obliga al navegador a revalidar siempre, con lo que el usuario ve cambios inmediatamente. s-maxage=600 deja que la CDN sirva desde su caché durante diez minutos sin molestar a tu origen, que es donde está el ahorro de coste. Y stale-while-revalidate=86400 hace que, cuando la copia del borde se pase de esos diez minutos, se sirva igualmente al instante mientras se refresca por detrás. El resultado es que tu origen recibe una fracción minúscula del tráfico y ningún usuario espera nunca a que se regenere.

⚠️
Las directivas desconocidas se ignoran, y eso corta en las dos direcciones

Una caché que no entiende immutable o stale-while-revalidate simplemente las ignora y aplica el resto de la cabecera. Eso es bueno, porque puedes usarlas sin romper nada. Y es malo, porque una directiva mal escrita, por ejemplo max_age con guion bajo en lugar de guion, también se ignora en silencio: no hay ningún error, tu recurso pasa a la regla heurística y tú crees que está cacheado un año. Comprueba siempre la cabecera efectiva en la respuesta real, no la que creíste configurar.

Cómo se verifica

Tres comprobaciones que hay que hacer siempre después de tocar una política de caché.

# 1. Ver la cabecera efectiva tal como sale del servidor.
curl -sI https://ejemplo.com/assets/app.a1b2c3.js | grep -i -E 'cache-control|etag|age|vary'

# 2. Comprobar que la revalidacion devuelve 304.
ETAG=$(curl -sI https://ejemplo.com/assets/app.a1b2c3.js | grep -i '^etag' | cut -d' ' -f2 | tr -d '\r')
curl -sI -H "If-None-Match: $ETAG" https://ejemplo.com/assets/app.a1b2c3.js | head -1

# 3. Comprobar que el borde esta sirviendo desde su cache.
curl -sI https://ejemplo.com/ | grep -i -E '^age|^x-cache'

Y en el navegador, la comprobación definitiva es que el recurso no toque la red en la segunda carga:

const desdeCache = performance.getEntriesByType('resource')
  .filter((r) => r.transferSize === 0 && r.decodedBodySize > 0)
  .map((r) => r.name);

console.log('servidos desde cache:', desdeCache.length);

El caso del despliegue

Una consecuencia práctica que hay que planificar. Si sirves tu HTML con max-age=3600, un despliegue tarda hasta una hora en llegar a un usuario que ya lo tenía cacheado, y no hay forma de forzarlo: la caché del navegador no se puede purgar desde el servidor.

Por eso la política estándar para el HTML es no cachearlo en el navegador, o cachearlo con max-age muy corto, y poner la duración larga en la CDN con s-maxage, donde sí puedes purgar. El HTML es el punto de entrada y es el que enlaza con los nombres versionados de todo lo demás: si el HTML está fresco, el usuario recibe la versión correcta de todos los estáticos aunque estos estén cacheados un año.

El valor de max-age que eliges para el HTML determina cuánto tardas en poder arreglar un incidente, y esa es la variable que hay que optimizar, no el ahorro de bytes

Al fijar la caché del HTML, el instinto es maximizar el ahorro y poner un valor generoso. La pregunta correcta es otra y es operativa: si ahora mismo hay un error grave en producción y desplegamos el arreglo, ¿cuánto tarda el último usuario en verlo? Con max-age=3600 en el navegador, la respuesta es una hora, y durante esa hora no puedes hacer absolutamente nada, porque la caché del navegador no se purga desde fuera. Con max-age=0 más s-maxage alto y purga en la CDN, la respuesta es segundos y no pierdes casi nada de rendimiento, porque el usuario sigue recibiendo la página desde el borde. He visto un equipo pasar dos horas de incidente esperando a que expirasen cachés de navegador que ellos mismos habían configurado a seis horas por ahorrar tráfico de origen. El ahorro que perseguían era de céntimos; el coste fue de dos horas de servicio degradado sin ninguna palanca. La regla que se deriva es corta: la caché del navegador para documentos HTML es una decisión de operaciones, no de rendimiento, y el valor por defecto correcto es cero con revalidación.