La petición más rápida es la que nunca ocurre. La caché HTTP es el mecanismo que lo hace posible — y también la fuente clásica del bug "ya he subido el cambio y no se ve". Este artículo desmonta cómo deciden navegador, CDN y servidor qué guardar, cuándo revalidar y qué servir mientras actualizan.
Los dos tipos de caché que importan
Toda respuesta HTTP puede almacenarse en dos sitios con reglas distintas:
- Caché privada (el navegador): solo para ti. Puede guardar contenido personalizado.
- Caché compartida (CDN, proxies): una copia para miles de usuarios. Guardar aquí contenido por-usuario es la fuga de datos más clásica del sector.
La directiva que separa ambos mundos:
Cache-Control: max-age=3600 ← cualquiera cachea 1 hora
Cache-Control: private, max-age=600 ← solo el navegador
Cache-Control: no-store ← nadie guarda nada (bancos, salud)
no-store no es lo mismo que no-cache: este último significa "guarda pero revalida siempre antes de usar" — útil y malentendido constantemente.
max-age vs s-maxage: dos relojes distintos
Cuando CDN y navegador conviven, cada uno merece política propia:
Cache-Control: public, max-age=60, s-maxage=86400, stale-while-revalidate=300
Leyenda: el navegador revalida cada minuto (cambios visibles rápido), el CDN retiene 24 horas (origen protegido), y durante los primeros 300 segundos tras caducar se sirve ligeramente rancio mientras se refresca en segundo plano. Esta combinación es el punto dulce de casi cualquier página HTML dinámica detrás de CDN.
Para assets fingerprinted (app.a83f2.js), la política opuesta es la correcta:
Cache-Control: public, max-age=31536000, immutable
Un año + immutable (el navegador ni pregunta). El truco: si el contenido cambia, cambia el nombre del fichero — el hash en el nombre ES tu invalidación. Es exactamente lo que hacen Next/Vite/Webpack al buildear.
Validación condicional: ETag y Last-Modified
Cuando la caché caduca, no hay que volver a descargar todo ciegamente. El mecanismo de validación compara versiones:
Primera respuesta:
ETag: "33a64df5"
Last-Modified: Wed, 25 Aug 2026 10:00:00 GMT
Siguientes peticiones, la caché pregunta:
If-None-Match: "33a64df5"
If-Modified-Since: Wed, 25 Aug 2026 10:00:00 GMT
Si no cambió, el servidor responde 304 Not Modified: sin cuerpo, unos cientos de bytes. El cliente sigue usando su copia local. Si cambió, 200 con contenido fresco.
Diferencia práctica entre ambos: ETag es un identificador arbitrario (hash de contenido, versión...) que detecta cualquier cambio; Last-Modified solo tiene resolución de segundo. Para contenido generado dinámicamente dentro del mismo segundo o donde mtime miente, ETag gana. Cuidado extra: ETags calculados por defecto a partir de atributos de fichero difieren entre servidores de un cluster — genera ETags de contenido, no de metadatos.
stale-while-revalidate y sus hermanos
Las directivas modernas resuelven el dilema frescura-latencia:
stale-while-revalidate=N: tras caducar, sirve la copia vieja inmediatamente Y refresca en background. El usuario nunca espera.stale-if-error=N: si el origen está caído, sigue sirviendo la copia vieja hasta N segundos. Resiliencia gratis ante micro-caídas.
El coste honesto: durante esa ventana los usuarios ven contenido ligeramente desfasado. Para feeds sociales o dashboards, equilibrio perfecto; para precios o stock, ajusta N a cero o usa invalidación activa.
Invalidación activa: purge
A veces necesitas borrar ya: errata publicada, precio incorrecto. Las CDNs exponen APIs de purga (Cloudflare, Fastly, CloudFront) por URL, tag o todo. Estrategia madura: TTLs largos + purga dirigida por tags de contenido (purge-by-tag: articulo-42) supera a TTLs cortos que castigan a todos los visitantes.
Ojo: la purga NO alcanza la caché privada del navegador. Un asset sin hash publicado con max-age largo queda envenenado para quien ya lo descargó hasta que expire — lección aprendida con lágrimas en media industria: los HTML nunca llevan caché larga; la larga es solo para ficheros con hash.
Vary: el parámetro olvidado
Vary declara qué cabeceras de petición generan variantes distintas de la misma URL:
Vary: Accept-Encoding
Sin esto, una caché compartida podría entregar la versión brotli-comprimida a un cliente que solo habla gzip. Otro caso real: servir imágenes AVIF/WebP/JPG negociando por Accept exige Vary: Accept, o los usuarios verán formatos que no soportan. Cada valor extra en Vary multiplica entradas de caché — úsalo con precisión, no como comodín.
Auditoría rápida
DevTools → Network → columna Size te lo cuenta sin herramientas externas: (disk cache), (memory cache) = servido localmente; tamaño completo = viaje real. La pestaña Headers muestra qué directivas emite cada recurso. Para verlo como un visitante externo, nuestro comprobador de cabeceras muestra las cabeceras completas que tu servidor emite de verdad — imprescindible porque intermediarios (hosting, CDN) modifican lo que tú configuras. El contexto completo de todas las cabeceras está en la guía de cabeceras.
Preguntas frecuentes
¿Cuánto tiempo debe cachear un HTML? Dinámico: no-cache (revalida siempre, ahorra bytes con 304) o max-age=60,s-maxage=300,SWR. Estático tipo blog: minutos en navegador, horas en CDN con purga disponible.
¿Ctrl+F5 fuerza saltarse la caché? Sí, pide con Cache-Control: no-cache ignorando copias locales — primer paso de diagnóstico cuando "no veo mi cambio".
¿Service Worker sustituye a Cache-Control? Se complementan: SW decide programáticamente qué servir (estrategias offline); Cache-Control gobierna el comportamiento HTTP subyacente. Un SW mal escrito puede servir basura eterna ignorando tus cabeceras.
Inspecciona las cabeceras de caché reales de tu web con nuestro HTTP Headers Checker, gratis y directamente en tu navegador.