Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Cache-Aside no Node.js com Redis e TTL

Atualizado em: 27 de outubro de 2026

Rack de servidores processando fluxos de dados no Node.js

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

  1. calcule a chave;
  2. consulte o Redis;
  3. se houver valor, valide e retorne;
  4. se não houver, consulte o banco;
  5. grave no cache com TTL;
  6. 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:

  1. consulte memória;
  2. consulte Redis;
  3. 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.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita