wandres.dev
CACHÉ HTTP · Las cabeceras que importan

Validación: ETag, Last-Modified y las peticiones condicionales

Cómo se comprueba si una copia cacheada sigue valiendo sin transferirla, la diferencia entre validadores fuertes y débiles, y por qué un ETag mal generado puede costar más de lo que ahorra.

⏱ 17 min

Cuando una respuesta cacheada se vuelve rancia no hay que descargarla otra vez: basta con preguntar si sigue valiendo. Ese intercambio, una petición condicional y una respuesta sin cuerpo, ahorra la transferencia entera a cambio de un viaje de ida y vuelta. Es la segunda mejor cosa después de no tocar la red, y funciona con dos mecanismos que conviene no mezclar.

🎯 Al terminar esta lección sabrás
  • Explicar el ciclo completo de una petición condicional con cada tipo de validador.
  • Distinguir validadores fuertes de débiles y saber cuándo hace falta cada uno.
  • Generar un ETag que sea correcto y barato de calcular.
  • Reconocer las tres configuraciones que rompen la validación sin que se note.

El ciclo de validación

El servidor adjunta a la respuesta uno o dos validadores: una etiqueta de entidad y una fecha de última modificación.

HTTP/1.1 200 OK
Content-Type: text/css
Cache-Control: public, max-age=600
ETag: "a1b2c3d4e5f6"
Last-Modified: Mon, 03 Aug 2026 14:22:10 GMT

Cuando la copia se vuelve rancia y hay que comprobarla, el navegador envía una petición condicional devolviendo los validadores que tiene:

GET /estilos.css HTTP/1.1
Host: ejemplo.com
If-None-Match: "a1b2c3d4e5f6"
If-Modified-Since: Mon, 03 Aug 2026 14:22:10 GMT

Y el servidor responde una de dos cosas. Si el recurso no ha cambiado:

HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4e5f6"
Cache-Control: public, max-age=600

Sin cuerpo. El navegador actualiza las cabeceras de su copia, con lo cual la entrada vuelve a estar fresca otros diez minutos, y la sirve. Si ha cambiado, el servidor responde 200 con el cuerpo nuevo y los validadores nuevos.

El ahorro es la transferencia, no el viaje. Para un CSS de 80 KB en una conexión lenta, eso es mucho. Para un icono de 400 bytes, el 304 puede costar más que el propio recurso, porque las cabeceras de la petición condicional pesan más que el cuerpo.

Si el navegador tiene ambos validadores, envía ambos, y el ETag tiene precedencia. El servidor debe evaluar primero If-None-Match e ignorar If-Modified-Since si aquel está presente.

Fuertes y débiles

Hay dos grados de validador y la distinción tiene consecuencias.

Un validador fuerte garantiza que si dos respuestas tienen el mismo valor, sus cuerpos son byte a byte idénticos. Un ETag normal es fuerte:

ETag: "a1b2c3d4e5f6"

Un validador débil solo garantiza equivalencia semántica: el contenido significa lo mismo, aunque los bytes difieran. Se marca con el prefijo W/:

ETag: W/"a1b2c3d4e5f6"

Un caso típico de validador débil es una página generada dinámicamente cuyo contenido es el mismo pero que incluye una marca de tiempo de generación, o dos representaciones que solo difieren en el nivel de compresión.

La diferencia importa en un caso concreto: las peticiones de rango. Para reanudar una descarga o pedir un fragmento de un vídeo, el cliente necesita estar seguro de que el fragmento que pide pertenece exactamente a la misma versión que ya tiene. Eso exige un validador fuerte. Con uno débil, el servidor debe rechazar la petición de rango y devolver el recurso completo.

Last-Modified es intrínsecamente débil, y por una razón concreta: su resolución es de un segundo. Si un archivo se modifica dos veces dentro del mismo segundo, la fecha no distingue las versiones y el cliente puede quedarse con la vieja creyendo que es la nueva. Esta es la razón principal por la que ETag es preferible.

ℹ️
Qué pasa cuando el servidor comprime

Si tu servidor comprime al vuelo, el cuerpo que llega depende del algoritmo de compresión, así que el ETag de la respuesta comprimida no debería ser el mismo que el de la sin comprimir. Algunos servidores lo resuelven marcando el ETag como débil al comprimir; otros lo modifican añadiendo un sufijo. Ambas soluciones son válidas; lo que rompe es que dos representaciones distintas compartan un ETag fuerte.

Generar un ETag correcto

Hay tres formas, con distintos compromisos.

Hash del contenido. El más correcto: dos respuestas con el mismo cuerpo tienen el mismo ETag, siempre, en todos los servidores. El coste es calcular el hash, que para archivos estáticos se hace en tiempo de construcción y no cuesta nada en ejecución.

import { createHash } from 'node:crypto';

function etagDe(buffer) {
  const hash = createHash('sha256').update(buffer).digest('base64url');
  return `"${hash.slice(0, 27)}"`;    // 27 caracteres bastan de sobra
}

Versión más identificador. Para contenido dinámico, componer el ETag con el identificador del recurso y su número de versión o su marca de tiempo de actualización en base de datos:

function etagDeRegistro(registro) {
  return `"${registro.id}-${registro.version}"`;
}

Es barato y correcto siempre que la versión se incremente en cada cambio real.

Metadatos del sistema de ficheros. Es lo que hacen muchos servidores por defecto: componer el ETag con el número de inodo, el tamaño y la fecha de modificación. Funciona en un único servidor y falla en cuanto hay varios, porque el número de inodo difiere entre máquinas y la fecha de modificación cambia al desplegar aunque el contenido sea idéntico. El resultado es que cada servidor genera un ETag distinto para el mismo archivo, y un usuario que va a parar a otra máquina recibe un 200 completo en lugar de un 304.

Este último caso es la causa número uno de validación rota en producción, y es tan silencioso que puede pasar años sin detectarse: todo funciona, simplemente se transfiere mucho más de lo necesario.

Las tres configuraciones que rompen la validación

Uno: ETag derivado de metadatos del sistema de ficheros con varios servidores. Ya descrito. La solución es generar el ETag desde el contenido en tiempo de construcción, o desactivarlo y confiar en Last-Modified si tus despliegues preservan las fechas.

Dos: Last-Modified que cambia en cada despliegue. Si tu proceso de despliegue copia archivos y les asigna la fecha actual, todos tus recursos parecen modificados aunque su contenido sea idéntico. Cada despliegue invalida toda la caché de todos los usuarios. Es sorprendentemente común y se detecta comparando la fecha de modificación de un archivo antes y después de un despliegue que no lo tocó.

Tres: intermediarios que eliminan o reescriben los validadores. Algunas configuraciones de proxy inverso eliminan ETag al hacer transformaciones. Se detecta comparando la respuesta del origen con la que llega al cliente.

# Comprobar que la validacion produce 304.
URL=https://ejemplo.com/estilos.css
ETAG=$(curl -sI "$URL" | grep -i '^etag:' | sed 's/^[^:]*: *//' | tr -d '\r')
echo "ETag recibido: $ETAG"
curl -sI -H "If-None-Match: $ETAG" "$URL" | head -1
# Se espera: HTTP/2 304

Cuándo la validación no compensa

La validación cuesta un viaje de ida y vuelta completo. Eso significa que solo compensa cuando el cuerpo que ahorras es mayor que lo que cuesta el viaje.

Con un RTT de 150 milisegundos y una conexión de 1,6 Mbps, un viaje equivale en tiempo a transferir unos 30 KB. Por debajo de ese tamaño, revalidar puede ser más lento que descargar.

Pero la comparación honesta no es esa, sino con no revalidar en absoluto. Y ahí la conclusión es clara: para recursos estáticos, la estrategia correcta no es afinar la validación sino eliminarla, poniendo un hash en el nombre del archivo y una frescura de un año con immutable. La validación es la solución para lo que no puede llevar hash en el nombre: documentos HTML, respuestas de API, imágenes subidas por usuarios.

Tipo de recurso Estrategia
Estáticos del build Hash en el nombre, un año, immutable. Sin validación
HTML Validación con ETag, max-age=0
Respuestas de API Validación con ETag derivado de la versión del dato
Imágenes de usuario Hash en el nombre si es posible; si no, validación
Archivos grandes descargables Validación con ETag fuerte, para permitir reanudar
Un 304 en una conexión móvil puede costar más que el recurso que ahorra, y por eso la métrica que importa no es el ratio de aciertos

El indicador que todo el mundo mira para juzgar una caché es el porcentaje de aciertos, contando como acierto tanto la respuesta servida sin tocar la red como el 304. Son dos cosas completamente distintas y mezclarlas oculta el problema. Una página con cuarenta recursos pequeños y una política que provoca cuarenta revalidaciones tiene un ratio de aciertos del cien por cien y está pagando cuarenta viajes de ida y vuelta que, en una red de 200 milisegundos y con multiplexación imperfecta, pueden ser más de un segundo de espera para no transferir treinta kilobytes en total. El indicador correcto es la proporción de peticiones que no tocan la red en absoluto, que en los tiempos de recurso del navegador se identifica con transferSize igual a cero. Cuando lo mides así, muchas cachés que parecían excelentes resultan estar revalidando casi todo, y la solución no es afinar los validadores sino cambiar la estrategia: poner el hash en el nombre y subir la frescura a un año. Se pasa de cuarenta viajes a cero, y el ratio de aciertos no cambia porque ya era del cien por cien.