Cache Invalidation no Node.js é o processo de remover ou atualizar dados armazenados em cache quando a fonte de verdade muda. É uma das partes mais difíceis de uma arquitetura de cache, porque o ganho de desempenho só é válido enquanto o conteúdo entregue continua correto.
Uma aplicação pode usar cache em memória, Redis, CDN e navegador ao mesmo tempo. Cada camada possui seu próprio tempo de vida, chave e mecanismo de purga. Sem uma estratégia explícita, dados antigos permanecem ativos, tenants se misturam e atualizações parecem desaparecer.
Fonte de verdade e cache
O banco ou serviço principal continua sendo a fonte de verdade. O cache é uma cópia descartável. A aplicação deve conseguir reconstruí-lo e operar, ainda que mais lentamente, quando ele estiver vazio ou indisponível.
TTL como primeira defesa
await redis.set(
`product:${productId}`,
JSON.stringify(product),
{ EX: 300 }
);O TTL limita por quanto tempo um erro de invalidação permanece visível. Ele não garante consistência imediata, mas evita valores eternamente obsoletos.
Invalidar após a gravação
await database.updateProduct(productId, changes);
await redis.del(`product:${productId}`);Excluir costuma ser mais seguro que tentar reconstruir o valor dentro da mesma operação. A próxima leitura buscará a versão atual no banco.
Falha entre banco e cache
Se o banco confirmar e a exclusão do cache falhar, o valor antigo continua disponível. Trate a invalidação como uma operação que pode falhar. Estratégias possíveis incluem retry, Outbox Pattern, TTL curto e reconciliação periódica.
Outbox para invalidação confiável
Grave a alteração e um evento na mesma transação:
BEGIN;
UPDATE products SET name = $2, version = version + 1 WHERE id = $1;
INSERT INTO outbox (type, aggregate_id)
VALUES ('product.changed.v1', $1);
COMMIT;Um worker publica o evento depois. Veja Outbox Pattern no Node.js.
Invalidação entre instâncias
Quando cada processo mantém cache local, uma instância não consegue apagar diretamente a memória das demais. Publique um sinal:
await publisher.publish(
'cache.products.invalidated.v1',
JSON.stringify({ productId })
);Cada instância remove sua chave local. Consulte Redis Pub/Sub no Node.js. Como Pub/Sub pode perder mensagens durante desconexões, combine com TTL.
Chaves versionadas
Uma versão no namespace permite invalidar um conjunto sem listar todas as chaves:
const namespaceVersion = await redis.get('products:namespace-version');
const key = `products:v${namespaceVersion}:item:${productId}`;Para uma purga global, incremente a versão. As chaves antigas expiram naturalmente.
Versão por entidade
const key = `product:${productId}:v${product.version}`;Essa técnica evita sobrescrever valores de versões diferentes. Ainda é necessário limpar ou expirar chaves antigas.
Tags de cache
Uma página pode depender de produto, categoria e preço. Registre quais chaves pertencem a cada tag:
SADD cache-tag:product:123 page:/products/123
SADD cache-tag:category:9 page:/category/9Ao alterar o produto, leia os membros e apague as páginas. Limite o tamanho dessas estruturas e remova membros expirados.
Invalidação por evento
Eventos devem ser versionados e conter somente os identificadores necessários:
{
"id": "evt-123",
"type": "product.changed.v1",
"data": {
"productId": "p-10",
"version": 8
}
}O consumidor pode ignorar um evento antigo se já processou versão maior.
Ordenação e corrida
Imagine duas atualizações rápidas. Uma invalidação atrasada não deve restaurar conteúdo antigo. Prefira excluir a chave, ou compare versões antes de gravar um novo valor.
if (incomingVersion > cachedVersion) {
await refreshCache();
}Double delete
Em alguns sistemas, a aplicação apaga o cache, atualiza o banco e apaga novamente depois de um pequeno intervalo. A técnica tenta reduzir uma corrida com leitores concorrentes, mas adiciona complexidade e não substitui versionamento ou eventos confiáveis.
Write-through
No write-through, a aplicação atualiza cache e fonte de verdade no mesmo fluxo. É útil quando existe uma camada responsável pela escrita, mas continua sujeito a falhas parciais. Defina qual sistema confirma primeiro e como reconciliar divergências.
Write-behind
No write-behind, a atualização entra no cache e é persistida depois. O desempenho pode melhorar, mas há risco de perda e ordenação. Use somente com fila durável, idempotência e requisitos que aceitem consistência eventual.
Negative caching
Resultados ausentes também podem ser armazenados por pouco tempo:
await redis.set(`product:${id}`, '__NOT_FOUND__', { EX: 30 });Quando o recurso é criado, invalide a chave negativa. Use TTL menor para não esconder criações recentes.
Cache por tenant
tenant:${tenantId}:product:${productId}O tenant deve fazer parte de toda chave relevante. Uma invalidação sem tenant pode apagar dados de outros clientes ou, pior, reutilizar conteúdo entre eles.
CDN e navegador
Respostas públicas podem usar Cache-Control, ETag e CDN. Para purgas, prefira URLs versionadas em assets e mecanismos do provedor para conteúdo dinâmico.
Cache-Control: public, max-age=60, stale-while-revalidate=300Veja Cache HTTP no Node.js.
Stale-while-revalidate
A aplicação pode servir temporariamente um valor antigo enquanto uma instância atualiza o cache. Isso reduz latência e stampede, mas deve ser usado apenas quando alguns segundos de atraso são aceitáveis.
Evite cache stampede
Quando uma chave popular expira, centenas de requisições podem consultar o banco. Use single-flight, lock curto ou TTL com jitter:
const ttl = 300 + Math.floor(Math.random() * 30);Consulte Cache Stampede no Node.js.
Reconciliação
Uma tarefa periódica pode comparar versões, apagar chaves antigas ou reconstruir índices de tags. Reconciliação é uma defesa contra eventos perdidos, mas deve ser paginada e limitada.
Observabilidade
Monitore:
- hit e miss rate;
- idade do valor;
- invalidações solicitadas e concluídas;
- falhas e retries;
- latência de reconstrução;
- chaves por namespace;
- eventos atrasados;
- divergências detectadas.
Não coloque IDs individuais em labels de métricas.
Testes de concorrência
- leitura durante atualização;
- duas atualizações fora de ordem;
- falha do Redis depois do commit;
- evento duplicado;
- evento perdido e recuperação por TTL;
- tenant diferente com mesmo ID;
- cache local em múltiplas instâncias;
- negative cache seguido de criação.
Erros comuns
- cache sem TTL;
- apagar antes do commit e não depois;
- ignorar falha da invalidação;
- chave sem tenant;
- reconstruir com versão antiga;
- depender apenas de Pub/Sub;
- não invalidar resultados negativos;
- purga global frequente;
- não medir idade do conteúdo.
Conclusão
Cache Invalidation no Node.js exige mais que chamar DEL. É necessário considerar falhas parciais, concorrência, múltiplas camadas e mensagens perdidas.
Combine TTL, eventos confiáveis, chaves versionadas e reconciliação. Mantenha o cache descartável e preserve o banco como fonte de verdade. A estratégia correta depende do atraso aceitável para cada dado.
Consulte a documentação do Redis sobre client-side caching e a documentação sobre keyspace notifications.


