DEV Community

Cover image for Caché de API con ETag y Cache-Control: Cómo las peticiones condicionales reducen tus payloads
Roobia
Roobia

Posted on Originally published at apidog.com

Caché de API con ETag y Cache-Control: Cómo las peticiones condicionales reducen tus payloads

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.

Pruebe Apidog hoy

HTTP ya resuelve este problema:

  • Cache-Control indica cuánto tiempo una respuesta permanece fresca.
  • ETag proporciona una huella digital para comprobar si cambió.
  • Las solicitudes condicionales pueden devolver 304 Not Modified sin reenviar el cuerpo.
  • If-Match protege 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
Enter fullscreen mode Exit fullscreen mode

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-revalidate ayudan 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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:

  1. El administrador A cambia el precio y guarda.
  2. El administrador B corrige un error tipográfico usando una copia antigua.
  3. 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
Enter fullscreen mode Exit fullscreen mode

El servidor compara If-Match con el ETag actual:

  • Coincide: aplica la actualización y devuelve 200 con un nuevo ETag.
  • No coincide: devuelve 412 Precondition Failed y 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.

  • private excluye la respuesta del almacenamiento de la CDN.
  • s-maxage=600 define un TTL específico para la CDN, independiente de max-age del 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);
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. Envíe GET /v1/products/42 y revise los encabezados de respuesta. Confirme que ETag y Cache-Control están presentes y que el ETag aparece entre comillas. Copie su valor.
  2. Añada If-None-Match con el valor copiado y envíe la solicitud otra vez. Debe recibir 304 con el cuerpo vacío.
  3. Modifique el registro, repita la solicitud y confirme que recibe 200 con un cuerpo actualizado y un ETag nuevo.

Para automatizar la comprobación después de cada despliegue:

  1. Envíe la primera solicitud y guarde el ETag de la respuesta en una variable.
  2. Envíe una segunda solicitud con ese valor en If-None-Match.
  3. Afirme que el estado es 304 y que el cuerpo está vacío.
  4. Añada una prueba de escritura con un If-Match obsoleto, por ejemplo "deadbeefcafe1234".
  5. 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)