O ETag e Cache HTTP no Node.js reduzem tráfego e trabalho do servidor ao permitir que clientes, proxies e CDNs reutilizem respostas. Em vez de transferir o mesmo JSON ou arquivo novamente, o cliente pode validar uma cópia local e receber apenas um status 304 Not Modified quando o conteúdo não mudou.
Uma política de cache correta melhora latência e capacidade, mas uma configuração errada pode expor dados privados, servir conteúdo antigo ou misturar respostas entre usuários. É necessário combinar ETag, Cache-Control, Vary, Last-Modified e autenticação de acordo com o tipo de recurso.
Neste guia, você aprenderá a gerar ETags, responder a If-None-Match, configurar Cache-Control, trabalhar com conteúdo público e privado, evitar cache poisoning, integrar com APIs e testar comportamento condicional.
O que é cache HTTP?
Cache HTTP permite armazenar uma resposta e reutilizá-la durante um período ou depois de validação. O RFC 9111 sobre HTTP Caching define diretivas e comportamento. O RFC 9110 sobre semântica HTTP descreve validators e requisições condicionais.
Para clientes HTTP, consulte Fetch Nativo no Node.js. Para conexões reutilizadas, veja HTTP Agent no Node.js.
O que é ETag?
ETag é um identificador da representação atual de um recurso:
ETag: "user-42-revision-8"Quando o cliente já possui essa versão, envia:
If-None-Match: "user-42-revision-8"Se a representação não mudou, o servidor responde:
HTTP/1.1 304 Not Modified
ETag: "user-42-revision-8"A resposta 304 não deve conter o corpo completo.
ETag forte
Um ETag forte indica equivalência byte a byte:
ETag: "a94a8fe5ccb19ba6"Ele é adequado quando duas representações com o mesmo identificador possuem exatamente o mesmo conteúdo.
ETag fraco
Um ETag fraco começa com W/:
ETag: W/"revision-8"Ele indica equivalência semântica suficiente para cache, mesmo que os bytes possam diferir, por exemplo por formatação.
Gerando ETag por hash
const { createHash } = require('node:crypto');
function createEtag(body) {
const hash = createHash('sha256')
.update(body)
.digest('base64url');
return `"${hash}"`;
}Gerar hash exige ter o corpo disponível. Em respostas grandes ou streaming, prefira versão armazenada, checksum previamente calculado ou metadado do recurso.
ETag baseado em revisão
function createResourceEtag(resource) {
return `"user-${resource.id}-v${resource.version}"`;
}O campo version deve mudar em toda alteração que afeta a representação.
Comparando If-None-Match
function matchesIfNoneMatch(header, etag) {
if (!header) return false;
if (header.trim() === '*') return true;
return header
.split(',')
.map(value => value.trim())
.includes(etag);
}A sintaxe completa possui detalhes de comparação forte e fraca. Bibliotecas do framework podem ser mais seguras do que um parser simplificado.
Resposta condicional
app.get('/api/users/:id', async (req, res) => {
const user = await repository.findById(req.params.id);
if (!user) {
return res.status(404).json({
code: 'USER_NOT_FOUND'
});
}
const body = JSON.stringify(serializeUser(user));
const etag = createEtag(body);
res.set('ETag', etag);
res.set('Cache-Control', 'private, max-age=0, must-revalidate');
if (req.get('if-none-match') === etag) {
return res.status(304).end();
}
res.type('application/json').send(body);
});Para erros assíncronos em Express, consulte Express 5: erros assíncronos.
Cache-Control
Cache-Control define quem pode armazenar e por quanto tempo:
Cache-Control: public, max-age=60Diretivas comuns:
public: caches compartilhados podem armazenar;private: apenas cache do cliente;no-store: não armazenar;no-cache: pode armazenar, mas deve revalidar;max-age: validade em segundos no cliente;s-maxage: validade em cache compartilhado;must-revalidate: não usar conteúdo expirado sem validação;immutable: conteúdo não muda durante a validade.
no-cache não significa não armazenar
no-cache permite armazenamento, mas exige validação antes do uso. Para dados que nunca devem ser gravados em cache, use no-store.
Dados privados
Respostas com informações pessoais ou tokens não devem ser públicas:
Cache-Control: private, no-storeUma resposta autenticada sem política correta pode ser armazenada por proxy compartilhado.
Conteúdo público
Catálogos públicos podem usar:
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30O navegador usa 60 segundos; a CDN pode usar 300 e servir conteúdo antigo por uma pequena janela enquanto atualiza.
Arquivos com hash no nome
/assets/app.a8f391c2.jsQuando o conteúdo muda, o nome muda. Isso permite:
Cache-Control: public, max-age=31536000, immutableNão use validade anual para um arquivo que mantém o mesmo nome e muda de conteúdo.
Vary
Vary informa quais headers mudam a representação:
Vary: Accept-Encoding, Accept-LanguageSem Vary, o cache pode entregar conteúdo comprimido, idioma ou versão errada.
Vary e autenticação
Não use Vary: Authorization como única proteção de dados privados. Prefira private ou no-store.
Versões de API
Quando a versão está em header, inclua-a em Vary:
Vary: X-API-VersionConsulte Versionamento de API no Node.js.
Last-Modified
Last-Modified: Fri, 21 Aug 2026 13:30:00 GMTO cliente pode enviar:
If-Modified-Since: Fri, 21 Aug 2026 13:30:00 GMTDatas possuem precisão limitada e podem ser menos confiáveis que uma revisão explícita.
ETag e Last-Modified juntos
É possível enviar ambos. O cliente usa validators conforme as regras HTTP. ETag costuma ser a referência mais precisa.
If-Match para concorrência otimista
ETag também protege atualizações:
PUT /api/users/42
If-Match: "user-42-v8"Se o recurso já está em v9, retorne 412 Precondition Failed. Isso evita sobrescrever mudanças de outra pessoa.
Atualização condicional
const expectedVersion = parseVersion(
req.get('if-match')
);
const updated = await repository.updateIfVersionMatches({
id,
expectedVersion,
changes
});
if (!updated) {
return res.status(412).json({
code: 'RESOURCE_CHANGED'
});
}If-None-Match com *
Em criação condicional:
PUT /api/documents/custom-id
If-None-Match: *A operação deve ocorrer apenas se o recurso não existe.
Paginação e cache
Cada combinação de cursor, filtro e ordenação representa outra resposta. A chave de cache precisa incluir toda a query normalizada.
Veja Paginação em APIs Node.js.
Normalizando query strings
Estas URLs podem ser equivalentes:
?page=2&sort=name
?sort=name&page=2Uma CDN pode tratá-las como chaves diferentes. Normalize parâmetros aceitos ou configure a camada de cache.
Cache no servidor
Além do protocolo HTTP, a aplicação pode armazenar resultados em memória ou Redis. Mesmo assim, continue enviando headers corretos para o cliente.
Cache stampede
Quando uma entrada expira, muitas requisições podem recalcular ao mesmo tempo. Estratégias:
- stale-while-revalidate;
- lock por chave;
- jitter no TTL;
- refresh antecipado;
- limite de concorrência.
Invalidar cache
Invalidação pode ocorrer por:
- mudança de versão;
- evento do banco;
- webhook;
- publicação de conteúdo;
- expiração curta;
- purge na CDN.
Evite depender de remoção manual sem auditoria.
ETag de conteúdo comprimido
Uma representação gzip possui bytes diferentes da versão sem compressão. Um ETag forte pode precisar variar por encoding. Combine com Vary: Accept-Encoding ou use ETag fraco semântico.
Compressão
JSON e texto podem ser comprimidos pelo servidor ou proxy. Para fundamentos, consulte Zlib no Node.js.
Streaming
Gerar hash ao final de um stream impede enviar ETag antes dos headers. Para arquivos, use checksum armazenado; para relatórios dinâmicos, talvez ETag não seja apropriado.
Cache de erros
Alguns erros podem ser cacheados por proxies. Para falhas temporárias, defina política conservadora:
Cache-Control: no-storeUm 404 de recurso imutável pode receber TTL curto, se isso fizer sentido.
Cache poisoning
Um atacante tenta fazer o cache armazenar uma resposta maliciosa ou específica. Proteções:
- validar Host e headers;
- usar Vary correto;
- normalizar URLs;
- não refletir headers não confiáveis;
- separar conteúdo autenticado;
- configurar CDN explicitamente.
CORS e cache
Se Access-Control-Allow-Origin muda conforme Origin, use:
Vary: OriginConsulte CORS em APIs Node.js.
CDN
Uma CDN pode sobrescrever ou ignorar diretivas. Verifique a política real, chaves consideradas, cookies removidos e comportamento de purge.
Documentação OpenAPI
Documente headers ETag, If-None-Match, If-Match e respostas 304 ou 412. Consulte OpenAPI com Node.js.
Observabilidade
Monitore:
- cache hit e miss;
- respostas 304;
- bytes economizados;
- tempo de geração;
- invalidações;
- respostas 412;
- idade do conteúdo;
- chaves de alta cardinalidade.
Logs
logger.info({
route: req.route.path,
cacheStatus: 'revalidated',
statusCode: 304
}, 'request_completed');Não registre ETags derivados de dados sensíveis se eles puderem revelar versão ou existência de recursos.
Testes
Cubra:
- primeira resposta 200;
- If-None-Match correspondente;
- ETag antigo;
- múltiplos ETags no header;
- Vary;
- conteúdo autenticado;
- If-Match correto e incorreto;
- compressão;
- paginação;
- invalidação.
Teste de revalidação
test('retorna 304 para ETag atual', async () => {
const first = await request('/api/products/42');
const etag = first.headers.etag;
const second = await request('/api/products/42', {
headers: {
'if-none-match': etag
}
});
assert.equal(second.status, 304);
assert.equal(second.body.length, 0);
});Erros comuns
- Public em resposta privada: dados podem vazar.
- no-cache interpretado como no-store: a política não faz o esperado.
- Vary ausente: representações são misturadas.
- ETag sem mudar: clientes usam conteúdo antigo.
- Hash caro por requisição: cache aumenta CPU.
- Arquivo imutável sem hash no nome: atualização demora a aparecer.
- Cachear erro transitório: falha persiste.
Boas práticas
- Classifique conteúdo como público ou privado.
- Use no-store para dados sensíveis.
- Gere ETag por revisão confiável.
- Configure Vary.
- Use nomes com hash para assets.
- Teste 304 e 412.
- Monitore hit rate.
- Evite hash caro em streams.
- Revise regras da CDN.
- Documente validators.
Conclusão
O ETag e Cache HTTP no Node.js permitem reutilizar respostas e validar conteúdo sem transferi-lo novamente. ETag identifica a representação, enquanto Cache-Control define armazenamento, validade e revalidação.
A eficiência depende de segurança e precisão. Conteúdo privado deve permanecer fora de caches compartilhados, Vary precisa representar todas as diferenças e ETags devem mudar sempre que a resposta muda. Com testes condicionais e métricas de hit rate, cache HTTP reduz latência sem sacrificar consistência.




