Cache HTTP para APIs: Cache-Control, ETag e respostas 304
Sua API provavelmente envia o mesmo JSON milhares de vezes por dia. Um cliente solicita GET /v1/products/42, recebe 18 KB, repete a chamada cinco minutos depois e obtém os mesmos 18 KB. Nada mudou — mas você pagou novamente pela largura de banda, serialização e leitura do banco de dados.
O HTTP já resolve esse problema. Cache-Control informa por quanto tempo uma resposta permanece fresca. ETag fornece uma “impressão digital” para verificar se o recurso mudou.
Juntos, eles transformam solicitações repetidas em respostas 304 Not Modified com corpo vazio e ainda podem proteger suas escritas contra atualizações perdidas. As mesmas ideias também aparecem no lado do cliente; o cache de respostas de API no React é o outro lado dessa estratégia.
Neste guia, você verá:
- as três camadas do cache HTTP;
- o ciclo completo de uma resposta
304; - a diferença entre
no-cacheeno-store; - ETags fortes e fracas;
- concorrência otimista com
If-Match; - um exemplo funcional em Express;
- como validar tudo no Apidog.
As três camadas do cache HTTP
O cache HTTP para APIs envolve três decisões diferentes. Misturá-las costuma gerar configurações incorretas.
1. Frescor
Por quanto tempo o cliente pode reutilizar uma resposta sem consultar o servidor?
Cache-Control: max-age=60
Durante 60 segundos, o cliente usa a cópia local sem gerar tráfego de rede. É o cache hit mais barato — e também o mais arriscado, pois o cliente não detecta alterações até o temporizador expirar.
2. Validação
Quando a resposta fica obsoleta, o cliente não precisa baixá-la novamente. Ele pergunta se o recurso mudou usando a impressão digital recebida anteriormente.
Se nada mudou, o servidor responde:
304 Not Modified
A resposta não contém corpo. O padrão mais preciso usa ETag e If-None-Match. A alternativa mais antiga usa Last-Modified e If-Modified-Since, com granularidade de um segundo.
3. Invalidação
Quando os dados mudam, como as cópias antigas deixam de ser usadas?
- Caches privados expiram naturalmente com
max-age. - Caches compartilhados e CDNs precisam de purgas explícitas, TTLs curtos ou diretivas como
stale-while-revalidate. - Essas diretivas limitam por quanto tempo uma resposta obsoleta pode ser servida.
Em resumo:
- frescor reduz solicitações;
- validação reduz payloads quando o frescor expira;
- invalidação mantém as cópias coerentes.
A maioria das APIs precisa das três camadas.
Como funciona uma viagem de ida e volta 304 Not Modified
Considere um endpoint de produto.
Primeira solicitação
O cliente ainda não possui uma cópia em cache:
GET /v1/products/42 HTTP/1.1
Host: api.example.com
Primeira resposta
O servidor retorna o corpo e os metadados de cache:
HTTP/1.1 200 OK
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
Content-Length: 18432
O cliente armazena o corpo e o ETag. Durante os próximos 60 segundos, ele não consulta o servidor.
Solicitação após 60 segundos
A cópia está obsoleta, então o cliente revalida:
GET /v1/products/42 HTTP/1.1
Host: api.example.com
If-None-Match: "33a64df551425fcc55e4d42a148795d9f2"
Recurso inalterado
O servidor compara o ETag recebido com o ETag atual. Como eles correspondem, retorna:
HTTP/1.1 304 Not Modified
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
Não há corpo. Em vez de 18 KB, a resposta contém apenas algumas centenas de bytes de cabeçalhos. O cliente marca a cópia como fresca por mais 60 segundos e a reutiliza.
Se o produto mudou, o servidor retornaria 200 OK com o novo corpo e um novo ETag. Consulte o explicador de 304 Not Modified para mais detalhes. A ideia principal é: 304 é uma instrução para o cache, não um erro.
Um GET condicional ainda exige uma viagem de ida e volta, autenticação e o cálculo ou busca do ETag atual. O que ele elimina é a transferência do payload e o reprocessamento no cliente. Em endpoints de lista grandes, especialmente para clientes móveis, isso pode reduzir a saída da API em 60% a 90%.
Diretivas Cache-Control importantes para APIs
O Cache-Control possui mais de uma dúzia de diretivas. Para APIs JSON, estas cinco são as mais relevantes.
no-store vs. no-cache
Essa é uma das fontes mais comuns de erros em APIs.
-
no-storesignifica: não armazene a resposta em nenhum cache. -
no-cachesignifica: você pode armazenar a resposta, mas deve revalidá-la antes de reutilizá-la.
Use no-store para dados realmente sensíveis, como:
- tokens;
- dados bancários;
- informações pessoais que não devem ser persistidas.
Com um ETag, no-cache ainda permite respostas 304 em cada revalidação. Assim, o cliente não exibe dados obsoletos e o servidor evita reenviar o payload completo.
Aplicar no-store a tudo “por segurança” desativa as solicitações condicionais e força o custo total do payload em cada chamada.
private
Indica que a resposta só pode ser armazenada pelo cliente final, e não por caches compartilhados ou CDNs.
Respostas que variam por usuário — a maior parte do tráfego de APIs autenticadas — devem usar private. Sem essa diretiva, um proxy mal configurado pode entregar os dados de uma conta para outro usuário.
max-age
Define o tempo de frescor em segundos.
Para APIs, valores entre 30 e 300 segundos costumam ser suficientes. O objetivo não é eliminar solicitações por um dia, mas absorver picos e loops de polling.
stale-while-revalidate
Permite servir uma cópia obsoleta enquanto o cache busca uma versão atualizada em segundo plano:
Cache-Control: max-age=60, stale-while-revalidate=300
Nesse exemplo, a resposta é fresca por 60 segundos e pode ser servida por até mais cinco minutos enquanto é revalidada.
Os usuários recebem respostas imediatas, e a origem é atualizada logo depois. CDNs como Cloudflare e Fastly oferecem suporte a essa diretiva, assim como os navegadores.
Um padrão razoável para um endpoint de leitura autenticado é:
Cache-Control: private, max-age=60, stale-while-revalidate=120
ETag: "9f8b2c41aa73e0d5"
A especificação completa está na RFC 9111, que substituiu a RFC 7234 como referência definitiva para cache HTTP.
ETags fortes e fracas
ETags têm dois formatos. O prefixo W/ identifica uma ETag fraca.
ETag forte
ETag: "33a64df551425fcc"
Promete igualdade byte a byte. Duas respostas com a mesma ETag forte são idênticas. Isso torna ETags fortes adequadas para requisições de faixa de bytes e necessárias para controle de concorrência com If-Match.
ETag fraca
ETag: W/"33a64df551425fcc"
Promete equivalência semântica, não igualdade byte a byte. A ordem dos campos ou um timestamp pode ter mudado sem alterar o significado do recurso.
Um caso problemático aparece com middleware de compressão. Nginx e alguns frameworks podem transformar ETags fortes em fracas ao compactar respostas em tempo real, pois os bytes compactados não correspondem mais aos bytes originais.
Se verificações de concorrência falharem atrás de um proxy, procure por um prefixo W/ que não existia quando a aplicação enviou a resposta.
Como padrão:
- use ETags fortes calculadas sobre o corpo não compactado;
- use ETags fracas apenas quando você aceita representações diferentes dos mesmos dados.
Gerando ETags: hash do corpo ou coluna de versão?
Há duas estratégias comuns.
Hash do corpo da resposta
Serialize a resposta e gere um hash:
ETag: "33a64df551425fcc55e4d42a148795d9f2"
MD5 ou SHA-1 são suficientes nesse caso. A ETag é uma impressão digital, não um mecanismo de segurança.
Vantagens:
- é precisa por construção;
- não exige alterações no esquema do banco.
Desvantagem:
- você precisa construir e serializar a resposta completa em cada solicitação, inclusive nas que terminam em
304.
Isso economiza largura de banda, mas não necessariamente CPU ou carga do banco.
Coluna de versão ou updated_at
Derive a ETag de um valor barato de consultar:
ETag: "42-v17"
A solicitação condicional pode exigir apenas uma busca indexada, em vez da serialização completa do recurso.
O cuidado é garantir que a versão seja incrementada para toda alteração que afete a resposta, inclusive alterações em tabelas relacionadas. Se uma mudança não atualizar a versão, o servidor poderá devolver um 304 obsoleto — um dos bugs de cache mais difíceis de detectar.
Comece com hash do corpo. Depois, mova endpoints muito acessados para ETags baseadas em versão quando a análise de perfil mostrar que a serialização é o gargalo.
Concorrência otimista com If-Match e 412
A mesma impressão digital usada para economizar largura de banda também evita atualizações perdidas.
Imagine este fluxo:
- Dois administradores carregam o produto 42.
- O Administrador A altera o preço e salva.
- O Administrador B corrige uma descrição 30 segundos depois.
- A alteração de B sobrescreve o preço atualizado por A.
Sem controle de concorrência, ninguém recebe um erro e os dados ficam silenciosamente incorretos.
A solução é tornar a atualização condicional à versão que o cliente recebeu:
PUT /v1/products/42 HTTP/1.1
If-Match: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
O servidor compara If-Match com a ETag atual:
- se corresponder, aplica a atualização e retorna
200com uma nova ETag; - se não corresponder, retorna
412 Precondition Failede não altera os dados; - se o cabeçalho não for enviado, APIs estritas podem retornar
428 Precondition Required.
O cliente deve buscar a versão atual, reaplicar sua alteração e tentar novamente. O guia sobre 412 Precondition Failed detalha esse código.
Depois que as ETags já existem, adicionar essa proteção custa pouco e transforma corrupção silenciosa de dados em um erro HTTP explícito e recuperável.
O que CDNs e proxies fazem com esses cabeçalhos
-
privateexclui a resposta do cache da CDN.s-maxage=600define um TTL específico para caches compartilhados, diferente domax-ageusado pelo navegador. - A maioria das CDNs revalida a resposta com solicitações condicionais. Se a origem responder
304, a CDN atualiza os metadados sem baixar novamente o corpo. - Confirme se o framework envia
Varycorretamente. Se a mesma URL puder retornar JSON ou CSV, use:
Vary: Accept
Caso contrário, um cache compartilhado pode entregar CSV a um cliente que solicitou JSON.
- Verifique se proxies estão enfraquecendo ETags durante a compressão.
Exemplo Express: ETag e If-None-Match
O Express gera ETags fracas por padrão. O tratamento manual abaixo usa ETags fortes e inclui o caminho de escrita com 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(); // impressão digital corresponde: sem corpo
}
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);
});
Observe que o ramo 304 ainda envia Cache-Control e ETag. Segundo a RFC 9111, o 304 atualiza os metadados da resposta armazenada. Reenvie tudo o que o cliente precisa para manter a cópia fresca.
Verificando o cache no Apidog
Código correto pode produzir cache incorreto quando middlewares e proxies entram em cena. Teste no nível HTTP, não apenas no código.
- Envie
GET /v1/products/42e abra os cabeçalhos da resposta. - Confirme que
ETageCache-Controlestão presentes e que a ETag está entre aspas. - Copie o valor da ETag.
- Adicione este cabeçalho à mesma requisição e envie novamente:
If-None-Match: "valor-copiado"
- Confirme que a resposta é
304e possui corpo vazio. - Altere o registro, repita a chamada e verifique se você recebe
200com uma ETag nova.
Para executar essa verificação após cada implantação, transforme o fluxo em um cenário de teste:
- a primeira requisição extrai a ETag dos cabeçalhos e salva o valor em uma variável;
- a segunda envia a variável como
If-None-Matche valida o status304e o corpo vazio; - uma terceira envia um
PUTcom uma ETag propositalmente obsoleta, como"deadbeefcafe1234", e valida o status412.
O guia de asserções de API mostra como validar códigos de status e cabeçalhos.
Execute esse cenário na CI. Assim, uma alteração de middleware que remova ETags silenciosamente falhará no pipeline, em vez de aumentar sua conta de largura de banda. Você pode baixar o Apidog gratuitamente e testar seus próprios endpoints.
FAQ
Qual é a diferença entre no-cache e no-store?
no-store proíbe completamente o armazenamento em cache. Nada é gravado em disco ou memória, e cada requisição baixa a resposta completa.
no-cache permite armazenar a resposta, mas exige revalidação antes de cada reutilização. Com uma ETag, isso ainda produz respostas 304 e economiza payload.
Use no-store para dados sensíveis, não como padrão global.
ETags funcionam com POST?
Na maioria dos casos, não — e isso é intencional.
ETags descrevem o estado de um recurso em uma URL, enquanto POST normalmente cria algo novo. Caches geralmente não armazenam respostas POST.
Para escritas, os cabeçalhos condicionais mais importantes são If-Match em PUT, PATCH e DELETE. Se você precisa armazenar o resultado de um POST em cache, avalie se a operação deveria ser um GET.
Uma resposta 304 torna minha API mais rápida?
Ela torna a transferência menor, o que não é exatamente a mesma coisa.
O servidor ainda precisa:
- receber a requisição;
- executar autenticação;
- calcular ou buscar a ETag atual.
As principais economias aparecem na largura de banda, na bateria do celular e no tempo de renderização em redes lentas. A carga de CPU na origem depende do custo para gerar a impressão digital.
Meça antes e depois. O guia de teste de desempenho de API ajuda a comparar latência e throughput.
Devo usar ETag ou Last-Modified?
Use ambos quando possível.
A ETag é mais precisa: detecta mudanças em submúltiplos de segundo e diferenças no conteúdo que um timestamp pode perder. Quando ambos estão presentes, If-None-Match tem prioridade sobre If-Modified-Since.
Last-Modified continua útil como fallback para clientes antigos e como heurística de frescor para alguns caches. Se você puder enviar apenas um, prefira ETag.
Para referência, consulte a documentação do Cache-Control e do ETag na MDN.
Top comments (0)