wandres.dev
CACHÉ HTTP · Las cabeceras que importan

El modelo de caché HTTP

Cómo decide un navegador si puede reutilizar una respuesta: frescura, caché privada y compartida, la clave de caché y el árbol de decisión completo.

⏱ 17 min

La caché HTTP es la única optimización que elimina la red por completo, y por tanto la única capaz de convertir una petición de trescientos milisegundos en una de uno. Antes de tocar una sola cabecera conviene tener claro el modelo: qué se guarda, quién lo guarda, cómo se identifica una respuesta guardada, y qué decisión toma exactamente el navegador cuando vuelves a pedir algo.

🎯 Al terminar esta lección sabrás
  • Distinguir caché privada de caché compartida y qué puede guardar cada una.
  • Explicar el concepto de frescura y el cálculo de edad de una respuesta.
  • Describir la clave de caché y qué la compone en un navegador moderno.
  • Recorrer el árbol de decisión completo de una petición cacheable.

Quién guarda qué

Hay dos clases de caché y la distinción determina qué se puede almacenar.

La caché privada pertenece a un solo usuario: la del navegador. Puede almacenar respuestas personalizadas, porque nadie más va a leerlas.

La caché compartida sirve a varios usuarios: una CDN, un proxy corporativo, un proxy inverso delante de tu aplicación. No puede almacenar respuestas personalizadas sin autorización explícita, porque serviría los datos de un usuario a otro. Este es el fallo de configuración más peligroso de todo el capítulo, y por eso las directivas distinguen ambos casos.

Además de estas dos, el navegador mantiene otros almacenes que no son la caché HTTP y que conviene no confundir: la caché de la API de almacenamiento que gestiona un service worker, que es programable y no obedece a las cabeceras; y la caché de retroceso y avance, que guarda la página entera en memoria con su estado de JavaScript y que no tiene nada que ver con este modelo.

Frescura y edad

El concepto central es la frescura. Una respuesta almacenada es fresca mientras no haya superado su tiempo de vida; pasado ese punto, se vuelve rancia.

Una respuesta fresca se sirve sin tocar la red. Una respuesta rancia no se descarta: se revalida, es decir, se pregunta al servidor si sigue valiendo, y si sigue valiendo se sirve la copia guardada. Esta distinción es clave, porque significa que una respuesta rancia sigue ahorrando la transferencia aunque no ahorre el viaje de ida y vuelta.

El tiempo de vida se obtiene, en orden de precedencia:

  1. La directiva s-maxage de Cache-Control, solo para cachés compartidas.
  2. La directiva max-age de Cache-Control.
  3. La cabecera Expires, con una fecha absoluta. Es de HTTP/1.0 y tiene menor precedencia.
  4. Una heurística, si no hay nada de lo anterior.

La cuarta opción es la que produce comportamientos inesperados. Si una respuesta no declara cuánto dura pero sí trae Last-Modified, muchas cachés aplican una regla heurística habitual: considerarla fresca durante aproximadamente el 10% del tiempo transcurrido desde la última modificación. Un archivo modificado hace cien días se cachearía diez días sin que nadie lo haya pedido. Es un comportamiento razonable como valor por defecto y una fuente de sorpresas cuando no sabes que existe.

La edad de una respuesta guardada se calcula desde la cabecera Date que puso el servidor de origen. Las cachés compartidas añaden además una cabecera propia que declara cuántos segundos lleva la respuesta almacenada:

HTTP/1.1 200 OK
Date: Tue, 04 Aug 2026 09:12:31 GMT
Cache-Control: public, max-age=3600
Age: 2847

Esa respuesta lleva 2.847 segundos en la caché compartida y le quedan 753 de frescura. La cabecera Age es la herramienta directa para saber si tu CDN está sirviendo desde su caché o yendo al origen: si viene con un valor alto, hubo acierto.

💡
La comprobación de treinta segundos para saber si tu caché funciona

Pide dos veces el mismo recurso estático y mira tres cosas en la segunda respuesta: si hay cabecera Age con valor creciente, si el navegador reporta transferSize igual a cero en los tiempos de recurso, y si el código de estado es 200 desde caché o 304. Si en la segunda petición ves un 200 completo con transferencia real, tu caché no está funcionando y ninguna otra optimización va a compensarlo.

La clave de caché

Para reutilizar una respuesta hay que poder encontrarla. La clave de caché es lo que la identifica, y en un navegador moderno está compuesta por más cosas de las que la gente supone.

La base es el método y la URL completa, incluidos los parámetros de consulta. Es decir, estilo.css?v=1 y estilo.css?v=2 son dos entradas distintas, que es precisamente lo que hace funcionar el versionado por parámetro.

A eso se añaden las cabeceras que indique Vary, que tratamos más adelante.

Y hay un componente adicional que cambió el panorama y que muchos artículos antiguos ignoran: la caché HTTP de los navegadores está particionada por sitio de nivel superior. Antes, si dos sitios distintos cargaban la misma biblioteca desde el mismo servicio público, el segundo sitio se beneficiaba de la caché del primero. Esa caché compartida entre sitios permitía rastrear al usuario y detectar qué sitios había visitado, así que se eliminó: hoy cada sitio tiene su propia partición.

La consecuencia práctica es directa y desmonta una creencia muy extendida: cargar una biblioteca desde un servicio público ya no aprovecha la caché de otros sitios. Ese argumento, que fue la razón principal para usar servicios de fuentes y de bibliotecas compartidas, ha dejado de ser cierto. Hoy servir esos recursos desde tu propio origen es mejor, porque reutilizas la conexión ya abierta y te ahorras una cadena completa de establecimiento.

El árbol de decisión

Lo que hace el navegador cuando necesita un recurso, paso a paso.

  1. ¿Hay una entrada en caché para esta clave? Si no, petición a la red.
  2. ¿Tiene la respuesta no-store? No debería estar guardada. Petición a la red.
  3. ¿Tiene no-cache, o must-revalidate y está rancia? Hay que revalidar antes de usarla. Se envía una petición condicional.
  4. ¿Sigue fresca? Es decir, ¿su edad es menor que su tiempo de vida? Si sí, se sirve desde la caché sin tocar la red. Este es el caso bueno.
  5. ¿Está rancia pero tiene stale-while-revalidate y estamos dentro de esa ventana? Se sirve la copia rancia inmediatamente y se revalida en segundo plano.
  6. Está rancia. Se envía una petición condicional con los validadores que se tengan.
  7. ¿El servidor responde 304? La copia guardada sigue valiendo: se actualizan sus cabeceras y se sirve. Se ha ahorrado la transferencia pero no el viaje.
  8. ¿El servidor responde 200? Llega contenido nuevo, se sustituye la entrada.
  9. ¿Falla la red y hay stale-if-error? Se sirve la copia rancia dentro de esa ventana en lugar de un error.

El paso 4 es el único que elimina la red del todo, y por eso toda la estrategia de caché consiste en maximizar la proporción de peticiones que terminan ahí. Los pasos 5 y 9 son los que dan resiliencia. Los pasos 6 y 7 ahorran bytes pero no latencia, que es la magnitud que domina.

Qué se puede cachear

Por defecto, y sin cabeceras que digan lo contrario, se pueden almacenar respuestas a peticiones GET con ciertos códigos de estado, no solo el 200: también 203, 204, 206, 300, 301, 404, 405, 410, 414 y 501 son almacenables en las condiciones adecuadas.

Que un 404 sea cacheable sorprende y tiene sentido: si un recurso no existe, preguntarlo cien veces no va a cambiar la respuesta. Y también explica un fallo desconcertante: un despliegue en el que un archivo faltó momentáneamente puede dejar 404 cacheados en el borde durante horas después de que el archivo exista.

Las peticiones con métodos que tienen efectos, como POST, no se cachean en condiciones normales; lo que sí hacen es invalidar las entradas asociadas a esa URL.

El error de caché más caro no es cachear poco, es cachear una respuesta personalizada en una caché compartida

Todo el mundo teme servir contenido viejo, y ese miedo lleva a configuraciones excesivamente conservadoras que desperdician la mayor palanca de rendimiento que existe. El riesgo real y grave es el contrario y ocurre en una sola línea de configuración: marcar como public una respuesta que depende del usuario autenticado. Con eso, la primera persona que pida su panel deja su panel en la caché compartida, y la siguiente lo recibe. He visto ocurrir esto dos veces, y las dos por la misma causa: una regla genérica en la CDN que decía cachear todo lo que respondiera 200, escrita por alguien que optimizaba el rendimiento sin saber qué rutas devolvían datos personales. La defensa es una regla de disciplina que no cuesta nada: la política de caché se declara por ruta desde la aplicación, nunca por patrón desde la capa de infraestructura, y cualquier respuesta que dependa de una cookie de sesión lleva private de forma explícita aunque parezca redundante. Ser explícito cuesta un byte por respuesta; el fallo cuesta un incidente de datos personales.