⏱️ 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
  1. TL;DR
  2. Qué es la caché HTTP y por qué importa
  3. Cómo funciona la caché HTTP en detalle
    1. Cache-Control: la cabecera que fija las reglas
    2. ETag: la huella digital del contenido
    3. Last-Modified: el validador basado en tiempo
  4. Ejemplos prácticos
  5. Cómo empezar
  6. Casos de uso reales
  7. Errores comunes y buenas prácticas
  8. Comparativa con alternativas
  9. Profundizando
  10. Preguntas frecuentes
    1. ¿Cuál es la diferencia real entre no-cache y no-store?
    2. ¿Cómo se genera un ETag en la práctica?
    3. ¿Cache-Control aplica solo a peticiones GET?
    4. ¿Qué pasa si el CDN y el navegador tienen reglas de caché distintas?
    5. ¿Vale la pena usar ETag si ya tengo un max-age largo?
    6. ¿Cómo pruebo mi configuración de caché sin herramientas externas?
  11. 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";
}
RFC 9111 reemplazó a la RFC 7234 en 2022 como estándar vigente de caché HTTP. Foto de Miguel Ángel Padriñán Alba en Unsplash

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 e immutable con un index.html servido con no-cache es 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 header Vary cuando una respuesta cambia según Accept-Encoding o Authorization puede 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

MecanismoQué haceCuándo usarloLimitación
Cache-Control: max-ageEvita cualquier petición de red mientras el tiempo no expiróAssets estáticos, respuestas que cambian pocoRiesgo de servir contenido viejo si expira mal calculado
Cache-Control: no-cacheFuerza revalidación en cada request, permite 304Datos que cambian seguido pero soportan verificación rápidaSiempre hay al menos una ida y vuelta de red
Cache-Control: no-storeProhíbe guardar cualquier copiaDatos sensibles: pagos, tokens, sesionesCero beneficio de caché, siempre transferencia completa
ETag + If-None-MatchValida por hash de contenidoAPIs y recursos donde generar el hash es baratoRequiere calcular y comparar el hash en cada revalidación
Last-ModifiedValida por fecha de modificaciónArchivos donde ya existe un timestamp confiableGranularidad de un segundo, menos preciso que ETag
stale-while-revalidateSirve la copia vieja mientras revalida en segundo planoContenido donde un pequeño desfase es aceptableEl 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
Cada capa de la jerarquía de caché respeta las mismas cabeceras HTTP que el origen. Foto de Mohammad Rahmani en Unsplash

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

📱 ¿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.


Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.