Cache-Aside no Node.js é um padrão em que a aplicação consulta primeiro o cache e, em caso de ausência, busca a fonte de verdade, devolve o resultado e grava uma cópia temporária. O banco não conhece o cache; toda a coordenação fica no código da aplicação.
O padrão é simples, flexível e funciona bem com Redis. Porém, precisa de TTL, proteção contra stampede, invalidação após escritas, tratamento de falhas e chaves corretamente isoladas por tenant e versão.
Fluxo básico
- calcule a chave;
- consulte o Redis;
- se houver valor, valide e retorne;
- se não houver, consulte o banco;
- grave no cache com TTL;
- retorne o resultado.
async function getProduct(productId) {
const key = `product:${productId}`;
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
const product = await repository.findById(productId);
if (product) {
await redis.set(key, JSON.stringify(product), { EX: 300 });
}
return product;
}Cache não é fonte de verdade
O banco continua responsável pela integridade. Se o Redis for apagado, a aplicação deve reconstruir valores sob demanda. Evite depender do cache para dados que não existem em outro armazenamento durável.
Chaves previsíveis
tenant:${tenantId}:product:${productId}:v${schemaVersion}Inclua tenant, tipo do recurso e versão do formato. Não use JSON completo ou entrada arbitrária como chave, pois isso aumenta tamanho e dificulta invalidação.
Serialização
JSON é suficiente para muitos casos, mas datas e BigInt exigem tratamento. Adicione uma versão ao valor:
{
"schemaVersion": 2,
"cachedAt": "2026-07-26T12:00:00Z",
"data": { "id": "p-10", "price": 1999 }
}Se a versão não for suportada, trate como miss e reconstrua.
TTL
O TTL limita obsolescência e uso de memória:
const baseTtl = 300;
const jitter = Math.floor(Math.random() * 30);
await redis.set(key, payload, { EX: baseTtl + jitter });O jitter evita que milhares de chaves semelhantes expirem no mesmo segundo.
Escolhendo o TTL
Dados quase imutáveis podem ficar mais tempo. Preços, estoque e permissões exigem TTL curto ou invalidação imediata. Documente o atraso máximo aceitável por tipo de dado.
Negative caching
Quando um recurso não existe, consultas repetidas podem pressionar o banco:
const NOT_FOUND = '__NOT_FOUND__';
if (cached === NOT_FOUND) return null;
const product = await repository.findById(id);
if (!product) {
await redis.set(key, NOT_FOUND, { EX: 30 });
return null;
}Use TTL curto e invalide quando o recurso for criado.
Falha do Redis
Uma indisponibilidade do cache não deveria derrubar automaticamente a leitura do banco. Implemente fallback:
let cached;
try {
cached = await redis.get(key);
} catch (error) {
logger.warn({ error }, 'cache read failed');
}
if (cached) return parse(cached);
return repository.findById(id);Proteja o banco com limites de concorrência e load shedding, pois a falha do cache pode multiplicar a carga.
Cache stampede
Uma chave popular expira e muitas requisições consultam o banco ao mesmo tempo. Use single-flight local:
const inFlight = new Map();
async function oncePerKey(key, loader) {
if (inFlight.has(key)) return inFlight.get(key);
const promise = loader().finally(() => inFlight.delete(key));
inFlight.set(key, promise);
return promise;
}Em múltiplas instâncias, use lock curto ou stale-while-revalidate. Consulte Cache Stampede no Node.js.
Stale-while-revalidate
Armazene expiração lógica e física. Durante uma janela curta, sirva o valor antigo e apenas uma instância atualiza:
{
"freshUntil": 1793072400000,
"data": { "id": "p-10" }
}O TTL físico pode ser maior que freshUntil. Use somente quando consistência eventual for aceitável.
Escritas e invalidação
Depois de alterar o banco, apague a chave:
await repository.updateProduct(id, changes);
await redis.del(`product:${id}`);Se a exclusão falhar depois do commit, o cache permanece antigo. Combine TTL com Outbox ou eventos confiáveis. Leia Cache Invalidation no Node.js.
Atualizar ou apagar?
Apagar é simples e força reconstrução. Atualizar pode reduzir miss, mas cria risco de gravar uma representação incompleta ou fora de ordem. Prefira apagar quando a consulta de reconstrução for barata.
Corrida entre leitura e escrita
Uma leitura pode buscar dado antigo no banco enquanto outra requisição atualiza. Depois, a leitura grava o valor antigo no cache. Reduza o risco com versão:
const product = await repository.findById(id);
await redis.set(
`product:${id}:v${product.version}`,
JSON.stringify(product),
{ EX: 300 }
);Outra opção é invalidar novamente depois da escrita ou usar compare-and-set em um script Lua.
Batching com MGET
Para vários IDs, evite uma ida ao Redis por item:
const keys = ids.map(id => `product:${id}`);
const values = await redis.mGet(keys);Busque no banco apenas os misses e grave com pipeline. Preserve a ordem de saída e aplique limite ao tamanho do lote.
Pipelines
const multi = redis.multi();
for (const product of products) {
multi.set(`product:${product.id}`, JSON.stringify(product), { EX: 300 });
}
await multi.exec();Pipeline reduz round trips, mas não torna todas as operações atomicamente dependentes.
Cache local e Redis
Uma camada L1 em memória pode reduzir latência:
- consulte memória;
- consulte Redis;
- consulte banco.
A invalidação fica mais complexa. Use Pub/Sub e TTL curto na L1. Consulte Redis Pub/Sub no Node.js.
Proteção contra cache poisoning
Não grave uma resposta antes de validar autorização, status e schema. Nunca use dados de um usuário para preencher uma chave compartilhada sem incluir todas as dimensões relevantes.
Permissões e dados sensíveis
Evite armazenar respostas personalizadas em chaves globais. Para permissões, considere TTL muito curto e invalidação imediata. Criptografia em trânsito e controle de acesso do Redis continuam obrigatórios.
Timeouts
Cache deve ser rápido. Defina timeout menor que o do banco:
const cached = await Promise.race([
redis.get(key),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('cache timeout')), 100)
)
]);Use APIs canceláveis quando disponíveis e não deixe Promises abandonadas acumularem.
Observabilidade
Monitore:
- hit, miss e stale hit;
- latência do cache e do loader;
- erros e timeouts;
- tamanho dos valores;
- reconstruções por chave lógica;
- single-flight ativo;
- evictions;
- hit rate por tipo de recurso.
Uma hit rate alta não prova correção. Monitore também divergências e idade.
Testes
- hit;
- miss;
- Redis indisponível;
- JSON inválido;
- versão antiga do schema;
- negative caching;
- duas leituras simultâneas;
- atualização seguida de leitura;
- tenant com mesmo ID;
- lote com hits e misses misturados.
Erros comuns
- cache sem TTL;
- chave sem tenant;
- não validar o valor recuperado;
- falha do Redis derrubar o endpoint;
- concorrência ilimitada no miss;
- não invalidar após escrita;
- guardar respostas autorizadas em chave global;
- não limitar tamanho;
- usar o cache como armazenamento definitivo.
Conclusão
Cache-Aside no Node.js é um padrão eficiente quando a aplicação controla explicitamente leitura, reconstrução e invalidação. Ele oferece flexibilidade, mas transfere para o código a responsabilidade por consistência e resiliência.
Use chaves versionadas, TTL com jitter, fallback, proteção contra stampede e invalidação confiável. O cache deve melhorar o desempenho sem esconder erros de autorização ou substituir a fonte de verdade.
Consulte a documentação do Redis para Node.js e a documentação de client-side caching.




