Caché HTTP para APIs: respuestas 304, ETags y Express
Su API probablemente envía el mismo JSON miles de veces al día. Un cliente solicita GET /v1/products/42, recibe 18 KB, repite la solicitud cinco minutos después y obtiene los mismos 18 KB. Nada cambió, pero usted volvió a pagar por el ancho de banda, la serialización y la lectura de la base de datos.
HTTP ya resuelve este problema:
-
Cache-Controlindica cuánto tiempo una respuesta permanece fresca. -
ETagproporciona una huella digital para comprobar si cambió. - Las solicitudes condicionales pueden devolver
304 Not Modifiedsin reenviar el cuerpo. -
If-Matchprotege las escrituras contra actualizaciones perdidas.
Estas mismas ideas se aplican al cliente. Si ya leyó nuestra guía sobre almacenamiento en caché de respuestas API en React, aquí encontrará la parte del servidor.
En esta guía verá las tres capas del almacenamiento en caché HTTP, el ciclo completo de una respuesta 304, las diferencias entre no-cache y no-store, y una implementación funcional con Express.
Las tres capas del almacenamiento en caché HTTP
El almacenamiento en caché de una API implica tres decisiones distintas.
1. Frescura
¿Cuánto tiempo puede el cliente reutilizar una respuesta sin consultar al servidor?
Cache-Control: max-age=60
Durante 60 segundos, el cliente sirve la copia localmente sin tráfico de red. Es el acierto de caché más barato, pero también el más arriesgado: el cliente no detectará cambios hasta que expire el temporizador.
2. Validación
Cuando la respuesta caduca, el cliente no tiene que descargarla de nuevo. Puede preguntar si cambió enviando la huella digital recibida anteriormente.
ETag con If-None-Match es la opción precisa. Last-Modified con If-Modified-Since es la alternativa más antigua, basada en marcas de tiempo con granularidad de un segundo.
3. Invalidación
Cuando cambian los datos, ¿cómo se eliminan las copias obsoletas?
- Las cachés privadas expiran mediante
max-age. - Las cachés compartidas y las CDN suelen necesitar purgas explícitas.
- TTL cortos y
stale-while-revalidateayudan a limitar la caducidad.
La frescura reduce más solicitudes, la validación detecta cambios después de la caducidad y la invalidación mantiene las copias actualizadas. La mayoría de las APIs necesitan las tres capas.
Cómo funciona un ciclo 304 Not Modified
Supongamos un endpoint de producto.
Primera solicitud
El cliente todavía no tiene una copia en caché:
GET /v1/products/42 HTTP/1.1
Host: api.example.com
Primera respuesta
El servidor devuelve el cuerpo y sus metadatos:
HTTP/1.1 200 OK
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
Content-Length: 18432
El cliente almacena el cuerpo y el ETag. Durante los siguientes 60 segundos puede servir la respuesta sin contactar al servidor.
Solicitud condicional
Después de 60 segundos, la copia caduca y el cliente la revalida:
GET /v1/products/42 HTTP/1.1
Host: api.example.com
If-None-Match: "33a64df551425fcc55e4d42a148795d9f2"
Respuesta sin cambios
El servidor compara el ETag recibido con el actual. Si coinciden:
HTTP/1.1 304 Not Modified
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
La respuesta no tiene cuerpo. En lugar de reenviar 18 KB, solo se transmiten unos cientos de bytes de encabezados. El cliente marca su copia como fresca durante otros 60 segundos.
Si el producto cambió, el servidor devuelve 200 OK, el nuevo cuerpo y un nuevo ETag. Un 304 es una instrucción de caché, no un error. Consulte la explicación de 304 Not Modified para más detalles.
Un GET condicional todavía requiere un viaje de ida y vuelta, autenticación y el cálculo o consulta del ETag. Lo que elimina es la transferencia de la carga útil y su análisis en el cliente. En endpoints de listas grandes consultados por clientes móviles, esto puede reducir la salida de la API entre un 60 % y un 90 %.
Directivas Cache-Control importantes
Cache-Control tiene muchas directivas, pero estas son las más relevantes para APIs JSON.
no-store frente a no-cache
Es la confusión más común.
-
no-store: la respuesta no debe guardarse en ninguna caché. Úselo para tokens, datos bancarios y PII que no debe persistir. -
no-cache: la respuesta sí puede almacenarse, pero debe revalidarse antes de cada reutilización.
Con un ETag, no-cache todavía permite ahorrar la carga útil mediante respuestas 304, garantizando que el cliente no reutilice datos sin validar.
Aplicar no-store a todas las respuestas deshabilita por completo las solicitudes condicionales y obliga a descargar la carga útil en cada llamada.
private
Indica que solo el cliente del usuario final puede almacenar la respuesta. Las cachés compartidas y las CDN deben excluirla.
Las respuestas que varían por usuario —la mayoría del tráfico autenticado— deberían incluir private. De lo contrario, un proxy mal configurado podría servir los datos de una cuenta a otro usuario.
max-age
Define la vida útil de frescura en segundos. Para APIs, valores entre 30 y 300 segundos suelen ser suficientes. El objetivo no es eliminar solicitudes durante un día, sino absorber picos y bucles de sondeo.
stale-while-revalidate
Permite servir una copia obsoleta mientras se actualiza en segundo plano:
Cache-Control: max-age=60, stale-while-revalidate=300
Las cachés pueden servir la copia durante hasta cinco minutos adicionales mientras consultan al origen. Los usuarios reciben respuestas rápidas y el origen se actualiza poco después. Las CDN como Cloudflare y Fastly, además de los navegadores, admiten esta directiva.
Un valor predeterminado razonable para un endpoint de lectura autenticado sería:
Cache-Control: private, max-age=60, stale-while-revalidate=120
ETag: "9f8b2c41aa73e0d5"
La especificación definitiva está en RFC 9111, que reemplazó a RFC 7234.
ETags fuertes y débiles
El prefijo W/ distingue los dos tipos.
ETag fuerte
ETag: "33a64df551425fcc"
Promete igualdad byte por byte. Dos respuestas con el mismo ETag fuerte son idénticas. Son necesarios para solicitudes de rango de bytes y para el control de concurrencia con If-Match.
ETag débil
ETag: W/"33a64df551425fcc"
Promete equivalencia semántica. Los bytes pueden diferir —por ejemplo, por un cambio en el orden de los campos o una marca de tiempo—, pero el significado sigue siendo el mismo.
El middleware de compresión puede causar problemas. Nginx y algunos frameworks convierten ETags fuertes en débiles cuando comprimen respuestas sobre la marcha, porque los bytes comprimidos ya no coinciden con los originales. Si la concurrencia falla detrás de un proxy, busque un prefijo W/ inesperado.
Como regla general, use ETags fuertes calculados sobre el cuerpo sin comprimir. Use ETags débiles cuando sirva deliberadamente representaciones variantes de los mismos datos.
Cómo generar un ETag
Hay dos estrategias principales.
Hash del cuerpo
Serialice la respuesta y calcule un hash:
ETag: "33a64df551425fcc55e4d42a148795d9f2"
MD5 o SHA-1 son suficientes para esta finalidad: el hash es una huella digital, no un límite de seguridad.
Ventajas:
- Es preciso por construcción.
- No requiere cambios de esquema.
Desventaja:
- El servidor debe construir y serializar la respuesta completa incluso para un 304.
- Ahorra ancho de banda, pero no necesariamente CPU ni consultas a la base de datos.
Columna de versión o updated_at
Derive el ETag de un valor económico de consultar:
ETag: "42-v17"
Una solicitud condicional puede requerir solo una búsqueda indexada, no una serialización completa.
El inconveniente es que la versión debe cambiar cada vez que se modifica cualquier dato que afecte la respuesta, incluidos los cambios en tablas relacionadas. Si se omite una actualización, el servidor puede devolver un 304 obsoleto: el peor tipo de error porque es invisible para el cliente.
Comience con un hash del cuerpo. Cambie los endpoints más utilizados a ETags basados en versiones cuando el perfilado demuestre que la serialización es costosa.
Concurrencia optimista con If-Match y 412
Los ETags también protegen las escrituras.
Imagine que dos administradores cargan el producto 42:
- El administrador A cambia el precio y guarda.
- El administrador B corrige un error tipográfico usando una copia antigua.
- La escritura de B sobrescribe silenciosamente el precio de A.
La solución es exigir que cada actualización coincida con la versión que el cliente leyó:
PUT /v1/products/42 HTTP/1.1
If-Match: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
El servidor compara If-Match con el ETag actual:
-
Coincide: aplica la actualización y devuelve
200con un nuevo ETag. -
No coincide: devuelve
412 Precondition Failedy no modifica los datos.
El cliente debe volver a consultar el recurso, reaplicar el cambio sobre la versión actualizada y reintentar. Consulte la guía sobre 412 Precondition Failed.
Las APIs estrictas también pueden devolver 428 Precondition Required cuando un PUT no incluye If-Match. De esta forma, la comprobación se vuelve obligatoria.
Qué hacen las CDN y los proxies
Las cachés compartidas se encuentran entre el origen y los clientes y aplican sus propias reglas a estos encabezados.
-
privateexcluye la respuesta del almacenamiento de la CDN. -
s-maxage=600define un TTL específico para la CDN, independiente demax-agedel navegador. - La mayoría de las CDN revalidan con solicitudes condicionales. Si el origen responde 304, actualizan los metadatos sin volver a transferir el cuerpo.
- Una API que sirve JSON y CSV desde la misma URL debe enviar
Vary: Accept. De lo contrario, una caché compartida podría entregar CSV a un cliente que solicitó JSON. - La compresión puede debilitar los ETags, como vimos anteriormente.
Express: devolver ETags y manejar If-None-Match
Express establece ETags débiles por defecto. El siguiente ejemplo genera ETags fuertes y también protege la ruta de escritura con 412:
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use(express.json());
function etagFor(payload) {
const hash = crypto.createHash("sha1")
.update(JSON.stringify(payload))
.digest("hex");
return `"${hash}"`;
}
app.get("/v1/products/:id", async (req, res) => {
const product = await db.products.find(req.params.id);
const etag = etagFor(product);
res.set("Cache-Control", "private, max-age=60, stale-while-revalidate=120");
res.set("ETag", etag);
if (req.get("If-None-Match") === etag) {
return res.status(304).end(); // fingerprint matches: no body
}
res.json(product);
});
app.put("/v1/products/:id", async (req, res) => {
const product = await db.products.find(req.params.id);
const currentEtag = etagFor(product);
const ifMatch = req.get("If-Match");
if (!ifMatch) {
return res.status(428).json({ error: "If-Match header required" });
}
if (ifMatch !== currentEtag) {
return res.status(412).json({ error: "Resource changed since you fetched it" });
}
const updated = await db.products.update(req.params.id, req.body);
res.set("ETag", etagFor(updated));
res.json(updated);
});
La rama 304 sigue enviando Cache-Control y ETag. Según RFC 9111, una respuesta 304 actualiza los metadatos de la respuesta almacenada. Reenvíe todos los encabezados que el cliente necesite para mantener su copia fresca.
Verificar el almacenamiento en caché con Apidog
El código puede ser correcto y aun así comportarse mal cuando intervienen middleware o proxies. Pruebe el comportamiento real a nivel HTTP.
En Apidog, la verificación manual toma aproximadamente un minuto:
- Envíe
GET /v1/products/42y revise los encabezados de respuesta. Confirme queETagyCache-Controlestán presentes y que el ETag aparece entre comillas. Copie su valor. - Añada
If-None-Matchcon el valor copiado y envíe la solicitud otra vez. Debe recibir304con el cuerpo vacío. - Modifique el registro, repita la solicitud y confirme que recibe
200con un cuerpo actualizado y un ETag nuevo.
Para automatizar la comprobación después de cada despliegue:
- Envíe la primera solicitud y guarde el ETag de la respuesta en una variable.
- Envíe una segunda solicitud con ese valor en
If-None-Match. - Afirme que el estado es
304y que el cuerpo está vacío. - Añada una prueba de escritura con un
If-Matchobsoleto, por ejemplo"deadbeefcafe1234". - Afirme que la respuesta es
412.
La guía sobre aserciones de API explica la sintaxis para validar códigos de estado y encabezados.
Ejecute el escenario en CI. Así, una actualización de middleware que elimine silenciosamente los ETags hará fallar la tubería en lugar de aumentar su factura de ancho de banda. También puede descargar Apidog gratis y probar sus propios endpoints.
Preguntas frecuentes
¿Cuál es la diferencia entre no-cache y no-store?
no-store prohíbe completamente el almacenamiento en caché: no se guarda nada en disco ni en memoria y cada solicitud descarga la respuesta completa.
no-cache permite almacenar la respuesta, pero exige revalidarla antes de reutilizarla. Combinado con un ETag, todavía produce respuestas 304 y ahorra la transferencia de la carga útil.
Use no-store solo para datos sensibles. Aplicarlo a todo es uno de los errores de Cache-Control más costosos para una API.
¿Funcionan los ETags con POST?
En general, no. Los ETags describen el estado de un recurso en una URL y POST normalmente crea algo nuevo en lugar de leer un estado estable. Además, las cachés casi nunca almacenan respuestas POST.
Para escrituras, los encabezados condicionales importantes son If-Match en PUT, PATCH y DELETE. Si necesita almacenar una respuesta POST, compruebe primero si la operación debería ser un GET.
¿Una respuesta 304 hace más rápida mi API?
Hace más pequeñas las transferencias, pero no elimina el trabajo del servidor. El origen aún recibe la solicitud, ejecuta la autenticación y calcula o consulta el ETag actual.
Los beneficios principales aparecen en:
- Ancho de banda.
- Batería del dispositivo móvil.
- Tiempo de renderizado en redes lentas.
Mida el resultado antes y después. La guía de pruebas de rendimiento de API explica cómo medir latencia y rendimiento.
¿Debería usar ETag o Last-Modified?
Envíe ambos cuando sea posible.
ETag es más preciso: detecta cambios de subsegundos y diferencias de contenido que una marca de tiempo podría omitir. Si llegan ambos encabezados condicionales, If-None-Match tiene prioridad sobre If-Modified-Since.
Last-Modified sigue siendo útil para clientes antiguos y como heurística para algunas cachés. Si solo puede enviar uno, elija ETag.
Top comments (0)