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.
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.
- 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:
- La directiva
s-maxagedeCache-Control, solo para cachés compartidas. - La directiva
max-agedeCache-Control. - La cabecera
Expires, con una fecha absoluta. Es de HTTP/1.0 y tiene menor precedencia. - 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.
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.
- ¿Hay una entrada en caché para esta clave? Si no, petición a la red.
- ¿Tiene la respuesta
no-store? No debería estar guardada. Petición a la red. - ¿Tiene
no-cache, omust-revalidatey está rancia? Hay que revalidar antes de usarla. Se envía una petición condicional. - ¿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.
- ¿Está rancia pero tiene
stale-while-revalidatey estamos dentro de esa ventana? Se sirve la copia rancia inmediatamente y se revalida en segundo plano. - Está rancia. Se envía una petición condicional con los validadores que se tengan.
- ¿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.
- ¿El servidor responde 200? Llega contenido nuevo, se sustituye la entrada.
- ¿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.
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.