⏱️ Lectura: 14 min
Cuando Chrome muestra (from disk cache) junto a un archivo en la pestaña Network, esa petición nunca llegó al servidor. El navegador respondió solo, sin abrir una conexión, porque una cabecera HTTP le había dicho minutos antes cuánto tiempo podía confiar en lo que ya tenía guardado.
📑 En este artículo
- TL;DR
- Qué es la caché HTTP y por qué importa
- Cómo funciona la caché HTTP en detalle
- Ejemplos prácticos
- Cómo empezar
- Casos de uso reales
- Errores comunes y buenas prácticas
- Comparativa con alternativas
- Profundizando
- Preguntas frecuentes
- ¿Cuál es la diferencia real entre no-cache y no-store?
- ¿Cómo se genera un ETag en la práctica?
- ¿Cache-Control aplica solo a peticiones GET?
- ¿Qué pasa si el CDN y el navegador tienen reglas de caché distintas?
- ¿Vale la pena usar ETag si ya tengo un max-age largo?
- ¿Cómo pruebo mi configuración de caché sin herramientas externas?
- Referencias
Esa cabecera se llama Cache-Control y, junto con ETag, forma el mecanismo de caché HTTP que decide si tu servidor reenvía datos que el cliente ya posee. Entenderlo bien ahorra ancho de banda, baja la latencia percibida y evita el bug clásico de servir contenido viejo sin darte cuenta.
TL;DR
- Cache-Control: max-age=60 le dice al navegador que no vuelva a pedir el recurso durante 60 segundos, sin tocar la red.
- Un ETag es un hash del contenido: si no cambió, el servidor responde 304 sin body y ahorra ancho de banda.
- no-cache no significa “no cachear”: fuerza revalidar en cada request, pero permite responder 304 sin reenviar datos.
- no-store sí bloquea el guardado completo: úsalo solo para datos sensibles que nunca deben persistir en disco.
- stale-while-revalidate sirve contenido viejo de inmediato mientras revalida en segundo plano, sin bloquear al usuario.
- El header Vary le indica a la caché que guarde versiones distintas según Accept-Encoding o Authorization.
- RFC 9111 reemplaza a la RFC 7234 de 2014 como el estándar vigente de caché HTTP del IETF.
- Un archivo con hash en el nombre (app.a1b2c3.js) puede llevar max-age=31536000 e immutable sin riesgo de servir versión vieja.
Qué es la caché HTTP y por qué importa
Pensá en un bibliotecario que memoriza qué libros ya le prestó a cada visitante. Si volvés a pedir el mismo libro que te llevaste ayer, no va al depósito a buscarlo de nuevo: te pregunta si todavía lo tenés vos. Eso es, en esencia, la caché HTTP: un acuerdo entre cliente y servidor sobre cuánto tiempo una respuesta sigue siendo válida antes de necesitar confirmación.
El estándar que define ese acuerdo es la RFC 9111, publicada por el IETF y que reemplaza a la RFC 7234 de 2014. Ahí se especifican las reglas exactas de Cache-Control, ETag, Last-Modified y cómo interactúan navegadores, proxies y CDNs.
La caché HTTP no es un plugin ni una librería externa: vive en el protocolo mismo. Cada respuesta puede declarar sus propias reglas de caché, y cada intermediario (el navegador, un proxy corporativo, un CDN) debe respetarlas. Cuando esas reglas faltan o son incorrectas, el resultado son dos problemas opuestos: servidores que reciben tráfico redundante, o usuarios que ven datos desactualizados sin saber por qué.
Cómo funciona la caché HTTP en detalle
Cache-Control: la cabecera que fija las reglas
Es una cabecera de respuesta que el servidor agrega, y acepta varias directivas combinables con comas. Las más usadas son public (cualquier caché, incluidos CDNs, puede guardarla), private (solo el navegador del usuario, nunca un proxy compartido), max-age=N (segundos que la respuesta es válida sin revalidar), no-cache, no-store, must-revalidate e immutable.
El nombre no-cache confunde a casi todos: no prohíbe guardar la respuesta. Le exige al cliente que, antes de reutilizarla, la revalide contra el servidor con una petición condicional. Si nada cambió, el servidor responde 304 sin volver a enviar el body. no-store es la directiva que realmente prohíbe guardar cualquier copia, ni siquiera para revalidar después: se reserva para datos sensibles como estados de pago o tokens de sesión.
ETag: la huella digital del contenido
Un ETag es un identificador (normalmente un hash del contenido) que el servidor adjunta a la respuesta. En la siguiente petición, el navegador lo devuelve en la cabecera If-None-Match. Si el servidor calcula el mismo hash, sabe que el contenido no cambió y responde 304 Not Modified sin body, ahorrando la transferencia completa aunque el recurso pese varios megabytes.
Last-Modified: el validador basado en tiempo
Es el predecesor de ETag: el servidor envía la fecha de última modificación, y el cliente la devuelve en If-Modified-Since. Es menos preciso porque solo tiene granularidad de segundos, así que dos cambios en el mismo segundo pasan inadvertidos. Sigue siendo útil como respaldo cuando generar un hash de contenido es costoso.
sequenceDiagram
participant N as Navegador
participant S as Servidor
N->>S: GET /estilos.css
S-->>N: 200 OK, max-age=60
Note over N: guarda la respuesta en cache local
Note over N: segunda peticion 30s despues
Note over N: cache aun valida, cero trafico de red
El diagrama anterior muestra el caso simple: mientras max-age no expiró, el navegador ni siquiera intenta contactar al servidor. Cuando esa ventana se cierra, entra en juego la revalidación condicional con ETag:
sequenceDiagram
participant N as Navegador
participant S as Servidor
N->>S: GET /api/precios, If-None-Match=etag-abc123
alt contenido sin cambios
S-->>N: 304 Not Modified, sin body
else contenido cambio
S-->>N: 200 OK, nuevo ETag y body completo
end
Ejemplos prácticos
El ejemplo más simple: una ruta Express que le dice al navegador que no vuelva a pedir nada durante un minuto.
const express = require('express');
const app = express();
app.get('/saludo', (req, res) => {
res.set('Cache-Control', 'public, max-age=60');
res.send('Hola, esta respuesta se cachea 60 segundos');
});
app.listen(3000);
La primera petición trae el body completo. Durante los siguientes 60 segundos, cualquier fetch() o recarga del navegador a esa misma URL se resuelve localmente: ninguna petición sale a la red.
El segundo ejemplo agrega ETag para datos que cambian con frecuencia, donde no conviene fijar un max-age largo:
const crypto = require('crypto');
app.get('/api/precios', (req, res) => {
const datos = JSON.stringify({ dolar: 3.75, actualizado: '2026-09-19' });
const etag = crypto.createHash('sha1').update(datos).digest('hex');
res.set('ETag', '"' + etag + '"');
res.set('Cache-Control', 'no-cache');
if (req.headers['if-none-match'] === '"' + etag + '"') {
return res.status(304).end();
}
res.type('json').send(datos);
});
Con no-cache, el navegador revalida en cada request, pero si el precio del dólar no cambió, el servidor responde 304 y el endpoint ahorra transferir el JSON completo en cada llamada.
Para archivos estáticos versionados (el patrón que generan Webpack o Vite al construir app.a1b2c3.js), la configuración típica en nginx es mucho más agresiva:
location ~* \.(?:css|js|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
Cómo empezar
Para probar esto en un proyecto propio con Node.js:
npm install express
Copiá el primer ejemplo de arriba en un archivo server.js y arrancalo con node server.js. Después verificá las cabeceras sin abrir el navegador:
curl -sI http://localhost:3000/saludo | grep -i cache-control
Para confirmar que la revalidación con ETag funciona, primero pedí el recurso y guardá el valor del header ETag de la respuesta, y después repetí la petición mandándolo en If-None-Match:
curl -sI http://localhost:3000/api/precios | grep -i etag
curl -s -o /dev/null -w "%{http_code}\n" \
-H 'If-None-Match: "valor-copiado-arriba"' \
http://localhost:3000/api/precios
Si el segundo comando imprime 304, la revalidación condicional está funcionando. En el navegador, la pestaña Network de las devtools muestra lo mismo: una fila con status 304 y tamaño de transferencia casi cero, o directamente la etiqueta (from disk cache) cuando ni siquiera hubo revalidación.
Casos de uso reales
Los CDNs como Cloudflare o Fastly leen exactamente estas mismas cabeceras para decidir qué guardar en sus servidores edge, ubicados geográficamente cerca del usuario. Un Cache-Control: public, max-age=3600 en el origen se propaga a cada nodo edge, así que la mayoría de las visitas nunca llega al servidor original.
Las apps móviles con conexión limitada se benefician especialmente de ETag en endpoints de API: si el catálogo de productos no cambió desde la última sincronización, la respuesta 304 evita descargar de nuevo varios megabytes de JSON sobre una red 4G intermitente.
El patrón de nombre con hash (main.a1b2c3.css) combinado con immutable resuelve el problema de invalidación de otra forma: en vez de esperar a que expire un max-age, cada build genera un nombre de archivo nuevo cuando el contenido cambia, así que el navegador puede cachearlo para siempre con seguridad.
💡 Tip: combinar archivos con hash eimmutablecon unindex.htmlservido conno-cachees el patrón estándar de despliegue de SPAs: el HTML siempre se revalida, los assets versionados nunca.
Errores comunes y buenas prácticas
El error más frecuente es tratar no-cache como sinónimo de no-store. Un equipo que cree estar deshabilitando la caché con no-cache en realidad sigue permitiendo que el navegador guarde la respuesta, solo que revalidada. Para bloquear el guardado por completo, la directiva correcta es no-store.
⚠️ Ojo: olvidar el headerVarycuando una respuesta cambia segúnAccept-EncodingoAuthorizationpuede hacer que un CDN sirva la versión comprimida de un usuario a otro sin comprimir, o peor, que sirva una respuesta pensada para un usuario autenticado a otro distinto.
Otro error clásico es fijar un max-age alto en contenido que cambia sin previo aviso, como el HTML principal de una SPA. Cuando el equipo hace un deploy, parte de los usuarios sigue viendo la versión vieja durante horas porque su navegador todavía confía en la caché. Phil Karlton lo resumió hace décadas: solo hay dos cosas difíciles en ciencias de la computación, invalidar caché y nombrar variables.
Durante desarrollo local también aparece fricción: el navegador cachea agresivamente respuestas 200 aunque el código del servidor haya cambiado. La solución simple es activar “Disable cache” en la pestaña Network de las devtools mientras el servidor está abierto, en vez de depurar cabeceras que en producción sí funcionan bien.
Comparativa con alternativas
| Mecanismo | Qué hace | Cuándo usarlo | Limitación |
|---|---|---|---|
| Cache-Control: max-age | Evita cualquier petición de red mientras el tiempo no expiró | Assets estáticos, respuestas que cambian poco | Riesgo de servir contenido viejo si expira mal calculado |
| Cache-Control: no-cache | Fuerza revalidación en cada request, permite 304 | Datos que cambian seguido pero soportan verificación rápida | Siempre hay al menos una ida y vuelta de red |
| Cache-Control: no-store | Prohíbe guardar cualquier copia | Datos sensibles: pagos, tokens, sesiones | Cero beneficio de caché, siempre transferencia completa |
| ETag + If-None-Match | Valida por hash de contenido | APIs y recursos donde generar el hash es barato | Requiere calcular y comparar el hash en cada revalidación |
| Last-Modified | Valida por fecha de modificación | Archivos donde ya existe un timestamp confiable | Granularidad de un segundo, menos preciso que ETag |
| stale-while-revalidate | Sirve la copia vieja mientras revalida en segundo plano | Contenido donde un pequeño desfase es aceptable | El usuario puede ver datos desactualizados por unos segundos |
Profundizando
La directiva stale-while-revalidate, definida en la RFC 5861, cambia el modelo tradicional de “válido o inválido” por uno de tres estados: fresco, obsoleto pero usable, y expirado. Con Cache-Control: max-age=60, stale-while-revalidate=30, durante los primeros 60 segundos la respuesta se sirve directo; en los 30 siguientes se sigue sirviendo la copia vieja de inmediato mientras el cliente dispara en paralelo una revalidación que actualiza la caché para la próxima vez.
El header Vary define qué parte de la petición forma parte de la clave de caché. Vary: Accept-Encoding le dice a un proxy que guarde una copia comprimida y otra sin comprimir por separado. Sin ese header, un CDN podría entregar la versión gzip a un cliente que no la soporta, o mezclar respuestas pensadas para distintos usuarios si la clave de caché no incluye Authorization.
La distinción entre caché privada y compartida también importa: private le dice a proxies y CDNs que no guarden esa respuesta, reservándola solo para el navegador del usuario final. Es la directiva correcta para cualquier endpoint que devuelve datos personalizados por sesión, aunque el contenido en sí no sea confidencial.
HTTP/2 y HTTP/3 no modifican ninguna de estas reglas: la semántica de caché es una capa completamente separada del transporte. Un servidor que sirve HTTP/3 sobre QUIC sigue usando exactamente las mismas cabeceras Cache-Control y ETag que uno en HTTP/1.1.
💭 Clave: la caché HTTP no reduce trabajo del servidor solo cuando acierta: cada 304 evitado por un ETag mal calculado sigue costando una consulta a base de datos completa, aunque el body nunca viaje por la red.
flowchart TD
A["Navegador"] -->|"cache miss"| B["CDN edge"]
B -->|"cache miss"| C["Servidor origen"]
C -->|"200 OK + Cache-Control"| B
B -->|"200 OK + Cache-Control"| A
Esa jerarquía explica por qué una sola cabecera mal configurada en el origen se propaga: si el servidor origen envía Cache-Control: public, max-age=3600, tanto el CDN edge como el navegador final respetan esa misma ventana de una hora, sin que nadie tenga que configurar nada por separado en cada capa.
📖 Resumen en Telegram: Ver resumen
Tu próximo paso: abrí las devtools de tu navegador, pestaña Network, recargá cualquier sitio que uses seguido y contá cuántas filas muestran (from disk cache) o status 304 en vez de 200.
Preguntas frecuentes
¿Cuál es la diferencia real entre no-cache y no-store?
no-cache permite guardar la respuesta pero obliga a revalidarla antes de reutilizarla, así que puede terminar en un 304 sin reenviar el body. no-store prohíbe guardar cualquier copia, ni siquiera para revalidar después.
¿Cómo se genera un ETag en la práctica?
Lo más común es un hash (MD5, SHA-1) del contenido de la respuesta, o un identificador de versión interno si el recurso ya tiene uno, como el timestamp de última escritura en base de datos.
¿Cache-Control aplica solo a peticiones GET?
Aplica a cualquier método, pero solo tiene sentido práctico en respuestas idempotentes como GET y HEAD. Las respuestas a POST casi nunca se cachean, salvo que el servidor lo indique explícitamente.
¿Qué pasa si el CDN y el navegador tienen reglas de caché distintas?
Un CDN puede sobrescribir o ignorar ciertas directivas según su configuración (por ejemplo, forzar un TTL mínimo propio), pero por defecto ambos respetan la misma cabecera Cache-Control que envía el origen.
¿Vale la pena usar ETag si ya tengo un max-age largo?
Sí, como respaldo para cuando el usuario fuerza un refresh manual (Ctrl+Shift+R) o cuando el max-age expira: el ETag evita retransferir el body completo aunque la revalidación sí toque la red.
¿Cómo pruebo mi configuración de caché sin herramientas externas?
Con curl -I para inspeccionar cabeceras, y repitiendo la petición con el header If-None-Match copiado del ETag anterior para confirmar que el servidor responde 304 en vez de 200.
Referencias
- MDN: Cache-Control: referencia completa de todas las directivas y su sintaxis.
- MDN: ETag: cómo se genera, se compara y se usa junto a If-None-Match.
- RFC 9111: HTTP Caching: el estándar IETF vigente que reemplaza a la RFC 7234.
- RFC 5861: define las extensiones stale-while-revalidate y stale-if-error.
- web.dev: Prevent unnecessary network requests with the HTTP Cache: guía práctica orientada a rendimiento web.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
0 Comentarios