Recorre los encabezados de caché HTTP — Cache-Control, ETag, Last-Modified, If-None-Match — y cómo funciona realmente una solicitud condicional
Frescura y validación son dos trabajos separados en el cacheo HTTP — uno se salta la red por completo, el otro sigue preguntando pero puede responder con casi nada.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Recorre los encabezados de caché HTTP — Cache-Control, ETag, Last-Modified, If-None-Match — y cómo funciona realmente una solicitud condicional
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Dos preguntas separadas, dos grupos de encabezados separados
El cacheo HTTP responde a dos preguntas distintas, y los grupos de encabezados se corresponden claramente con ellas. La primera pregunta es "¿puedo saltarme por completo la consulta al servidor?" — respondida por las directivas de frescura de Cache-Control, verificadas puramente contra el reloj local, sin tráfico de red si la respuesta es sí. La segunda pregunta, que solo se hace cuando la respuesta a la primera es no, es "¿esto realmente cambió desde la última vez que lo vi?" — respondida por una solicitud condicional, usando ETag/If-None-Match o Last-Modified/If-Modified-Since, que sigue tocando la red pero puede volver con un 304 Not Modified vacío y diminuto en vez del cuerpo completo de la respuesta.
#Cache-Control: la capa de frescura
Las directivas de Cache-Control controlan si a una caché siquiera se le permite saltarse la red, y quién puede quedarse con la copia:
| Directiva | Significado |
|---|---|
max-age=N | Trata la respuesta como fresca durante N segundos — ninguna solicitud al servidor durante esa ventana. |
no-cache | No significa "no cachear" — significa cachéalo, pero revalida con el servidor cada vez antes de servirlo. |
no-store | La directiva real de "no cachear nada, en ningún lado" — nunca escribas la respuesta en ninguna caché. |
private | Solo el navegador del propio usuario final puede cachear esto; las cachés compartidas (CDNs, proxies) no deben hacerlo. |
public | Permite explícitamente que cachés compartidas/intermediarias lo almacenen, incluso para respuestas que de otro modo parecerían privadas (por ejemplo, con un encabezado Authorization). |
must-revalidate | Una vez obsoleta, la caché debe revalidar antes de servir — nada de servir lo obsoleto ante un error sin preguntar antes. |
stale-while-revalidate=N | Sirve la copia obsoleta de inmediato, y refréscala en segundo plano durante hasta N segundos. |
immutable | El contenido de esta URL exacta nunca cambiará — omite la revalidación por completo incluso en un refresco forzado. |
#Validadores: ETag y Last-Modified
Cuando una respuesta cacheada se vuelve obsoleta, el cliente no necesariamente necesita todo el cuerpo de nuevo — necesita saber si el cuerpo que ya tiene sigue siendo correcto. Para eso existe un validador.
Un ETag (entity tag) es una huella digital opaca de una representación específica de un recurso — normalmente un hash del contenido, o un identificador de versión. El servidor envía ETag: "abc123" con la respuesta; el cliente lo guarda junto al cuerpo cacheado; en la siguiente solicitud, pasado el max-age, el cliente envía If-None-Match: "abc123". Si el servidor calcula la misma etiqueta para el recurso actual, sabe que no hay nada que valga la pena reenviar y devuelve 304 Not Modified sin ningún cuerpo. Si el contenido cambió, devuelve 200 OK con el cuerpo nuevo y un ETag nuevo.
Last-Modified es el validador más antiguo y más burdo: una marca de tiempo de cuándo cambió el recurso por última vez, verificada mediante If-Modified-Since en la siguiente solicitud. Es barato de producir (a menudo es simplemente el mtime de un archivo) pero tiene una debilidad real — la mayoría de las implementaciones solo tienen resolución de un segundo, así que dos ediciones dentro del mismo segundo son indistinguibles, y el mtime de un archivo puede cambiar (por ejemplo, tras un redeploy que reescribe los archivos byte por byte de forma idéntica) sin que el contenido real cambie en absoluto. Los ETags no tienen ese problema porque se derivan del contenido, no de un reloj, razón por la cual las APIs modernas prefieren los ETags y mantienen Last-Modified sobre todo como respaldo para clientes o cachés que no entienden ETags.
Un detalle más que vale la pena conocer: los ETags pueden ser fuertes ("abc123", deben ser idénticos byte por byte para considerarse una coincidencia) o débiles (W/"abc123", basta con ser semánticamente equivalentes — útil cuando un servidor puede determinar barato que "esto significa lo mismo" pero no puede garantizar barato una salida idéntica byte por byte, por ejemplo tras una recompresión gzip).
#Una solicitud condicional real, de principio a fin
Primera solicitud — todavía no hay nada cacheado:
GET /api/products/42 HTTP/1.1Host: api.example.comAccept: application/json
HTTP/1.1 200 OKContent-Type: application/jsonCache-Control: max-age=60, must-revalidateETag: "33a64df551425fcc55e4d42a148795d9f25f89d"Last-Modified: Wed, 19 Aug 2026 14:22:31 GMTContent-Length: 48{"id":42,"name":"Wireless Mouse","price":29.99}
Sesenta segundos después, max-age expiró y el cliente debe revalidar antes de poder seguir usando su copia cacheada:
GET /api/products/42 HTTP/1.1Host: api.example.comAccept: application/jsonIf-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d"If-Modified-Since: Wed, 19 Aug 2026 14:22:31 GMT
Si el producto no cambió, el servidor responde sin ningún cuerpo — sin price, sin name, nada, solo encabezados confirmando que la copia del cliente sigue siendo válida:
HTTP/1.1 304 Not ModifiedETag: "33a64df551425fcc55e4d42a148795d9f25f89d"Cache-Control: max-age=60, must-revalidate
Ese 304 es todo el sentido de las solicitudes condicionales: el viaje de red de ida y vuelta igual ocurre, pero el payload que habría sido de decenas de kilobytes para un cuerpo de respuesta real se reduce a prácticamente nada.
#Implementándolo del lado del servidor
Así es como se ve, en la práctica, generar ese ETag y manejar la verificación condicional en una ruta de Express:
const express = require("express");const crypto = require("crypto");const app = express();function computeEtag(payload) {const hash = crypto.createHash("sha1").update(payload).digest("hex");return `"${hash}"`;}app.get("/api/products/:id", (req, res) => {const product = getProductById(req.params.id); // { id, name, price, updatedAt }const body = JSON.stringify(product);const etag = computeEtag(body);res.set("Cache-Control", "max-age=60, must-revalidate");res.set("ETag", etag);res.set("Last-Modified", product.updatedAt.toUTCString());// GET condicional: si la copia cacheada del cliente sigue vigente,// evita reenviar el cuerpo por completo.if (req.headers["if-none-match"] === etag) {return res.status(304).end();}res.status(200).type("application/json").send(body);});
La comparación es una simple verificación de cadenas contra el encabezado If-None-Match entrante — la parte cara (obtener product, calcular su JSON) igual tiene que ocurrir para conocer el ETag actual, pero el ahorro de ancho de banda en una coincidencia es real: un 304 con un par de encabezados en vez de un cuerpo JSON completo, multiplicado por cada cliente que vuelve a verificar un recurso que en realidad no cambió.
🧠 Check your understanding
0/3 · 0/3 answered1. ¿Qué significa realmente la directiva de Cache-Control "no-cache"?
2. ¿Qué contiene una respuesta 304 Not Modified?
3. ¿Por qué los ETags generalmente superan a Last-Modified en cuanto a corrección?