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

Aprenda cómo el encabezado Cache-Control y la validación ETag convierten las llamadas repetidas a la API en respuestas 304, evitan actualizaciones perdidas con If-Match y reducen el tamaño de la carga útil.

Ashley Goolam

Ashley Goolam

31 August 2026

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

Apidog para empresas

Despliegue local

SSO & RBAC

Conforme con SOC 2

Explorar Apidog Enterprise

Su API probablemente envía el mismo JSON miles de veces al día. Un cliente solicita GET /v1/products/42, recibe 18 KB, lo solicita de nuevo cinco minutos más tarde y obtiene los mismos 18 KB. Nada cambió. Usted pagó por el ancho de banda, la serialización y la lectura de la base de datos de todos modos.

HTTP ya resolvió este problema. El encabezado Cache-Control indica a los clientes cuánto tiempo una respuesta permanece "fresca". El encabezado ETag les proporciona una huella digital para verificar si ha cambiado. Juntos, convierten las solicitudes repetidas en respuestas 304 Not Modified con cuerpos vacíos, y pueden proteger sus escrituras de actualizaciones perdidas como un extra. Las mismas ideas también impulsan patrones del lado del cliente; si ha leído nuestra guía sobre el almacenamiento en caché de respuestas API en React, esta es la parte del servidor de esa historia.

Esta guía recorre las tres capas del almacenamiento en caché HTTP, muestra el viaje de ida y vuelta 304 paso a paso, desenreda no-cache vs no-store y termina con código Express funcional. También verá cómo verificar todo esto en Apidog enviando encabezados condicionales y afirmando el 304 usted mismo.

botón

Las tres capas del almacenamiento en caché HTTP

El almacenamiento en caché HTTP para APIs se divide en tres decisiones separadas. Los equipos se meten en problemas cuando las confunden.

Capa 1: Frescura. ¿Cuánto tiempo puede un cliente reutilizar una respuesta sin preguntarle a usted en absoluto? Eso es Cache-Control: max-age=60. Durante 60 segundos, el cliente sirve la copia en caché localmente. Cero tráfico de red. Este es el acierto de caché más barato posible y también el más arriesgado, porque el cliente no puede detectar un cambio hasta que el temporizador expire.

Capa 2: Validación. Una vez que la respuesta caduca, el cliente no tiene que volver a descargarla. Pregunta "¿ha cambiado esto?" enviando la huella digital que le dio antes. Si el recurso no ha cambiado, usted responde con un 304 Not Modified y sin cuerpo. ETag con If-None-Match es la versión precisa de esto; Last-Modified con If-Modified-Since es la versión más antigua, basada en marcas de tiempo, con granularidad de un segundo.

Capa 3: Invalidación. Cuando los datos cambian, ¿cómo mueren las copias caducadas? Las cachés privadas de los clientes expiran por sí solas a través de max-age. Las cachés compartidas y las CDNs necesitan purgas explícitas, TTLs cortos o directivas como stale-while-revalidate que limitan la caducidad.

La frescura es lo que más ahorra, la validación detecta todo lo que la frescura pasa por alto, y la invalidación mantiene la honestidad de ambas. La mayoría de las APIs necesitan las tres.

Cómo funciona un ciclo de ida y vuelta 304 Not Modified

Aquí está el ciclo completo para un endpoint de producto, paso a paso.

Primera solicitud. El cliente no tiene nada en caché:

GET /v1/products/42 HTTP/1.1
Host: api.example.com

Primera respuesta. Usted devuelve el cuerpo más los metadatos de caché:

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, no le contacta en absoluto.

Segunda solicitud, después de 60 segundos. La copia está caducada, por lo que el cliente revalida:

GET /v1/products/42 HTTP/1.1
Host: api.example.com
If-None-Match: "33a64df551425fcc55e4d42a148795d9f2"

Segunda respuesta, recurso sin cambios. Su servidor compara el ETag entrante con el actual. Coinciden, así que:

HTTP/1.1 304 Not Modified
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"

Sin cuerpo. En lugar de 18 KB, la respuesta son unos cientos de bytes de encabezados. El cliente marca su copia en caché como fresca por otros 60 segundos y la sirve. Si el producto hubiera cambiado, usted devolvería un 200 normal con el nuevo cuerpo y un nuevo ETag. Cubrimos el código de estado en sí con más profundidad en nuestra explicación del 304 Not Modified; la versión corta es que un 304 es una instrucción de caché, no un error.

La economía es simple. Un GET condicional sigue costando un viaje de ida y vuelta más el trabajo que calcule el ETag actual. Lo que elimina es la transferencia de la carga útil y el re-análisis del lado del cliente. Para endpoints de listas grandes encuestados por clientes móviles, esto reduce rutinariamente la salida de la API entre un 60 y un 90 por ciento.

Directivas Cache-Control que importan para las APIs

Cache-Control tiene más de una docena de directivas. Para las APIs JSON, cinco son las más importantes.

no-store vs no-cache. Este es el error de caché más común en las APIs de producción, y funciona en ambas direcciones. no-store significa "nunca escribir esto en ninguna caché". Úselo para cargas útiles genuinamente sensibles: tokens, datos bancarios, PII que no debe persistir. no-cache significa casi lo contrario de lo que parece: las cachés PUEDEN almacenar la respuesta, pero deben revalidar con el origen antes de cada reutilización. Junto con un ETag, no-cache le ofrece ahorros de 304 en cada solicitud, garantizando al mismo tiempo que los clientes nunca muestren datos obsoletos. Los equipos que aplican no-store a todo "para estar seguros" están deshabilitando por completo las solicitudes condicionales y pagando el costo total de la carga útil en cada llamada.

private. Marca la respuesta como almacenable en caché solo por el cliente del usuario final, nunca por cachés compartidas o CDNs. Cualquier respuesta que varíe por usuario, que es la mayoría del tráfico API autenticado, debe llevar private. Sin ella, un proxy mal configurado puede servir los datos de la cuenta de un usuario a otro.

max-age. Vida útil de frescura en segundos. Para APIs, piense en pequeño: de 30 a 300 segundos cubre la mayoría de los endpoints de lectura. No está tratando de eliminar solicitudes por un día; está tratando de absorber picos y bucles de sondeo.

stale-while-revalidate. El punto medio pragmático. Cache-Control: max-age=60, stale-while-revalidate=300 le dice a las cachés: sirva la copia obsoleta por hasta 5 minutos adicionales, pero actualícela en segundo plano. Los usuarios obtienen respuestas instantáneas; su origen se actualiza poco después. Las CDNs como Cloudflare y Fastly lo soportan, al igual que los navegadores.

Un valor predeterminado sensato para un endpoint de lectura autenticado se ve así:

Cache-Control: private, max-age=60, stale-while-revalidate=120
ETag: "9f8b2c41aa73e0d5"

La especificación completa del comportamiento se encuentra en RFC 9111, que reemplazó a RFC 7234 como el documento definitivo de almacenamiento en caché HTTP. Cuando una CDN se comporta de una manera que le sorprende, esa RFC es donde reside la respuesta.

ETags fuertes vs débiles

Un ETag viene en dos tipos, y el prefijo W/ los separa.

Un ETag fuerte (ETag: "33a64df551425fcc") promete igualdad byte por byte. Dos respuestas con el mismo ETag fuerte son idénticas, lo que hace que los ETags fuertes sean seguros para las solicitudes de rango de bytes y necesarios para el control de concurrencia con If-Match.

Un ETag débil (ETag: W/"33a64df551425fcc") promete equivalencia semántica. Los bytes pueden diferir, tal vez el orden de los campos cambió o un campo de marca de tiempo se actualizó, pero el significado es el mismo, por lo que una caché puede mantener su copia.

Aquí es donde le muerde: middleware de compresión. Nginx y algunos frameworks reescriben los ETags fuertes a débiles cuando comprimen una respuesta sobre la marcha, porque los bytes comprimidos ya no coinciden con los originales. Si sus comprobaciones de concurrencia fallan misteriosamente detrás de un proxy, busque un prefijo W/ que no estaba allí cuando su servidor de aplicaciones envió la respuesta.

Por defecto, use ETags fuertes calculados sobre el cuerpo sin comprimir. Use los débiles solo cuando a sabiendas sirva representaciones variantes de los mismos datos.

Generación de ETags: hash del cuerpo vs columna de versión

Dos estrategias dominan, y la correcta depende de dónde reside el costo.

Hash del cuerpo de la respuesta. Serialice la respuesta, hágale un hash (MD5 o SHA-1 está bien aquí; esto es una huella digital, no un límite de seguridad) y cítela. Es preciso por construcción y no necesita cambios de esquema. El inconveniente: usted construye la respuesta completa en cada solicitud, incluidos los 304s. Ahorra ancho de banda pero no carga de cómputo ni de base de datos.

Columna de versión o updated_at. Derive el ETag de datos que puede obtener de forma económica: ETag: "42-v17" del contador de versión de la fila, o un hash de updated_at. Ahora una solicitud condicional cuesta una búsqueda indexada en lugar de una serialización completa. El inconveniente: la versión debe actualizarse en cada cambio que afecte la respuesta, incluidos los cambios en las tablas unidas. Si omite uno, servirá 304s obsoletos, que es el peor error de caché porque es invisible.

Comience con el hashing del cuerpo. Es correcto por defecto. Mueva los endpoints "calientes" a ETags basados en versiones cuando el perfilado muestre que el costo de serialización importa.

ETags para concurrencia optimista: If-Match y 412

La misma huella digital que ahorra ancho de banda en lecturas previene actualizaciones perdidas en escrituras.

El problema de la actualización perdida: dos administradores cargan el producto 42 al mismo tiempo. El administrador A cambia el precio y lo guarda. El administrador B corrige un error tipográfico y lo guarda 30 segundos después, sobrescribiendo el cambio de precio de A con el precio obsoleto que B cargó. Nadie ve un error. Los datos son silenciosamente incorrectos.

La solución es hacer que cada actualización sea condicional a la versión que el cliente vio por última vez:

PUT /v1/products/42 HTTP/1.1
If-Match: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json

El servidor compara If-Match con el ETag actual del recurso. Coincidencia: aplica la actualización, devuelve 200 con un nuevo ETag. No hay coincidencia, alguien más llegó primero: rechaza con 412 Precondition Failed y no toca los datos. El cliente entonces vuelve a buscar, reaplica su cambio en la versión fresca y reintenta. Las APIs estrictas van más allá y devuelven 428 Precondition Required en cualquier PUT que omita If-Match, haciendo que la verificación de seguridad sea obligatoria.

Esto no le cuesta casi nada añadir una vez que los ETags existen, y convierte un error de corrupción de datos silencioso en un estado HTTP explícito y reintentable.

Qué hacen las CDNs y los proxies con estos encabezados

Las cachés compartidas se sitúan entre su origen y sus clientes, y leen los mismos encabezados según sus propias reglas.

Ejemplo de Express: devolviendo un ETag y manejando If-None-Match

Express establece ETags débiles por sí mismo, pero el manejo manual le proporciona ETags fuertes más la ruta de escritura 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);
});

Tenga en cuenta que la rama 304 aún envía los encabezados Cache-Control y ETag. Según la RFC 9111, un 304 actualiza los metadatos de la respuesta almacenada, por lo que reenvíe todo lo que el cliente necesite para mantener su copia fresca.

Verificando el comportamiento del almacenamiento en caché en Apidog

El código que parece correcto aún puede almacenar en caché de forma incorrecta una vez que los middleware y los proxies intervienen. Pruebe a nivel HTTP, no a nivel de código.

En Apidog, la verificación manual toma aproximadamente un minuto:

  1. Envíe GET /v1/products/42 y abra el panel de encabezados de respuesta. Confirme que ETag y Cache-Control están presentes y que el ETag está entre comillas. Copie el valor del ETag.
  2. En la misma solicitud, añada un encabezado If-None-Match con el valor copiado y envíe de nuevo. Debería obtener un 304 con un cuerpo vacío. Si aún obtiene un 200, su capa de validación no está comparando huellas digitales.
  3. Cambie el registro, reenvíe y confirme que vuelve a obtener un 200 con un ETag nuevo.

Para mantener esto funcionando después de cada despliegue, conecte el mismo flujo a un escenario de prueba. Encadene dos solicitudes: la primera extrae el ETag de los encabezados de respuesta a una variable, la segunda lo envía de vuelta como If-None-Match y afirma que el estado es igual a 304 y el cuerpo está vacío. Añada un tercer paso para la ruta de escritura: envíe un PUT con un valor If-Match deliberadamente obsoleto como "deadbeefcafe1234" y afirme 412. Nuestra guía sobre aserciones de API cubre la sintaxis de aserción para códigos de estado y encabezados.

Ejecute ese escenario en CI y una actualización de middleware que elimine silenciosamente sus ETags se convierte en una tubería fallida en lugar de una factura de ancho de banda. Descargue Apidog gratis y construya el escenario contra sus propios endpoints; lleva más tiempo leer sobre ello que configurarlo con clics.

Preguntas Frecuentes

¿Cuál es la diferencia entre no-cache y no-store?

no-store prohíbe completamente el almacenamiento en caché: no se escribe nada en disco o memoria, por lo que cada solicitud descarga la respuesta completa. no-cache permite almacenar, pero fuerza la revalidación antes de cada reutilización, por lo que, combinado con un ETag, aún produce respuestas 304 y ahorros de carga útil. Use no-store solo para datos sensibles. Usarlo en todas partes es el error de Cache-Control más costoso que puede cometer un equipo de API.

¿Funcionan los ETags con POST?

En su mayoría no, y por diseño. Los ETags describen el estado de un recurso en una URL, y POST generalmente crea algo nuevo en lugar de leer un estado estable. Las cachés no almacenan en caché las respuestas POST en la práctica. Los encabezados condicionales que importan para las escrituras son If-Match en PUT, PATCH y DELETE, donde el ETag protege contra actualizaciones perdidas. Si está tentado a almacenar en caché las respuestas POST, eso suele ser una señal de que la operación debería ser un GET.

¿Una respuesta 304 hace que mi API sea más rápida?

Hace que las transferencias sean más pequeñas, lo cual no es lo mismo. El servidor aún recibe la solicitud, ejecuta la autenticación y calcula el ETag actual, por lo que el ahorro de CPU del origen depende de cuán económicamente derive esa huella digital. Los beneficios se manifiestan en el ancho de banda, la batería del móvil y el tiempo de renderizado en redes lentas. Mida antes y después; nuestra guía de pruebas de rendimiento de API muestra cómo medir la latencia y el rendimiento para que pueda demostrar la diferencia en lugar de adivinar.

¿Debería usar ETag o Last-Modified?

Envíe ambos cuando pueda. ETag es más preciso: detecta cambios de subsegundos y diferencias a nivel de contenido que una marca de tiempo pasa por alto, e If-None-Match tiene prioridad sobre If-Modified-Since cuando ambos llegan. Last-Modified sigue siendo útil como alternativa para clientes más antiguos y como heurística que algunas cachés usan para estimar la frescura. Si solo envía uno, envíe ETag.

Practica el diseño de API en Apidog

Descubre una forma más fácil de construir y usar APIs