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

Cache Stampede no Node.js

Atualizado em: 3 de outubro de 2026

Rack de servidores processando fluxos de dados no Node.js

Cache stampede acontece quando uma entrada popular expira e muitas requisições tentam reconstruí-la ao mesmo tempo. O banco ou serviço de origem recebe uma rajada inesperada, a latência aumenta e a falha pode se espalhar por toda a aplicação.

O problema também é chamado de dogpile effect ou thundering herd. Em Node.js, ele aparece em caches locais, Redis, CDNs e funções que memorizam Promises sem controlar expiração.

Exemplo vulnerável

async function getProduct(id) {
  const key = `product:${id}`;
  const cached = await redis.get(key);

  if (cached) return JSON.parse(cached);

  const product = await database.findProduct(id);
  await redis.set(key, JSON.stringify(product), { EX: 300 });
  return product;
}

Se mil requisições chegam logo após o TTL, todas leem miss e consultam o banco.

Request coalescing local

Dentro de um processo, armazene a Promise em andamento:

const inFlight = new Map();

async function coalesce(key, loader) {
  if (inFlight.has(key)) return inFlight.get(key);

  const promise = Promise.resolve()
    .then(loader)
    .finally(() => inFlight.delete(key));

  inFlight.set(key, promise);
  return promise;
}

Uso:

async function getProduct(id) {
  const key = `product:${id}`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  return coalesce(key, async () => {
    const secondCheck = await redis.get(key);
    if (secondCheck) return JSON.parse(secondCheck);

    const product = await database.findProduct(id);
    await redis.set(key, JSON.stringify(product), { EX: 300 });
    return product;
  });
}

O segundo check evita reconstruir se outro fluxo preencheu o cache.

Limite do coalescing local

Cada pod possui seu próprio Map. Dez réplicas ainda podem executar dez reconstruções. Para operações caras, use coordenação distribuída.

Lock distribuído

Um lock no Redis pode permitir um único regenerador:

const lockKey = `lock:${key}`;
const token = crypto.randomUUID();

const acquired = await redis.set(lockKey, token, {
  NX: true,
  PX: 5000,
});

Se adquiriu, carrega e preenche. Se não, espera curto período, lê novamente ou usa stale.

Liberando o lock com segurança

Não execute DEL sem verificar o token, pois o lock pode ter expirado e sido adquirido por outro processo. Use script atômico:

if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('DEL', KEYS[1])
end
return 0

TTL do lock

O lock deve expirar para evitar deadlock após crash. Mas um TTL curto pode vencer antes da reconstrução, permitindo outro worker. Defina prazo realista e, se necessário, renovação controlada.

Espera limitada

Quem não adquiriu o lock não deve aguardar indefinidamente:

for (let attempt = 0; attempt < 5; attempt += 1) {
  await sleep(50 + Math.random() * 100);
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
}

throw new Error('Cache em reconstrução');

Uma alternativa melhor é servir stale.

Stale-while-revalidate

Armazene valor, freshUntil e staleUntil:

{
  "value": { "id": 42 },
  "freshUntil": 1790841600000,
  "staleUntil": 1790841900000
}

Se fresh, devolva. Se stale, devolva imediatamente e inicie atualização em background. Se expirou completamente, aguarde reconstrução.

Implementação conceitual

async function getWithStale(key, loader) {
  const entry = await cache.get(key);
  const now = Date.now();

  if (entry && entry.freshUntil > now) {
    return entry.value;
  }

  if (entry && entry.staleUntil > now) {
    void coalesce(key, () => refresh(key, loader));
    return entry.value;
  }

  return coalesce(key, () => refresh(key, loader));
}

A atualização em background precisa de logging, timeout e tratamento de rejeição.

stale-if-error

Se a origem falha, uma resposta antiga pode preservar disponibilidade:

try {
  return await refresh(key, loader);
} catch (error) {
  if (entry && entry.errorStaleUntil > Date.now()) {
    return entry.value;
  }
  throw error;
}

Não use stale para saldo, autorização ou dados que exigem consistência forte.

Jitter de TTL

Muitas chaves podem expirar juntas após deploy ou importação. Adicione variação:

function ttlWithJitter(baseSeconds, ratio = 0.2) {
  const variation = baseSeconds * ratio;
  return Math.round(baseSeconds - variation + Math.random() * variation * 2);
}

Probabilistic early expiration

Uma entrada pode ser atualizada antes de expirar com probabilidade crescente. Isso distribui regenerações sem esperar o instante exato do TTL.

Uma abordagem simples define refreshAfter menor que expiresAt e permite que uma requisição assuma atualização por lock.

Refresh ahead

Um job atualiza chaves populares antes do vencimento. É útil para catálogo e configurações conhecidas, mas pode desperdiçar recursos em itens não acessados.

Hot keys

Uma chave extremamente popular pode sobrecarregar o Redis mesmo com hit. Estratégias:

  • cache local curto;
  • replicação;
  • CDN;
  • sharding de leitura quando semântica permitir;
  • resposta pré-computada;
  • limite por cliente.

Cache local em camadas

L1 no processo e L2 no Redis:

requisição → L1 → L2 → banco

L1 reduz tráfego, mas aumenta janela de inconsistência. Use TTL curto e invalidação quando necessária.

Negative caching

Ausência também pode causar stampede. Cacheie “não encontrado” por TTL curto:

await cache.set(key, { found: false }, { ttl: 30 });

Depois de criar o recurso, invalide a chave negativa.

Erros em cache

Não cacheie indiscriminadamente toda exceção. Um erro transitório de banco não deve virar resposta persistente. Cache negativo é para resultado válido de ausência.

Timeout do loader

Uma Promise em andamento pode nunca concluir. Use AbortSignal e remova o mapa em finally.

Fila limitada

Se a origem está lenta, milhares de chaves diferentes podem criar milhares de loaders. Use semáforo global para limitar reconstruções simultâneas.

Load shedding

Quando a fila excede capacidade, rejeite operações menos importantes ou sirva stale. Aguardar indefinidamente aumenta p99 e memória.

Cache warming

Após deploy ou flush, pré-carregue apenas chaves críticas. Aquecer todo o catálogo pode ser tão pesado quanto um stampede.

Flush perigoso

Evite FLUSHALL em produção. Invalide por namespace ou versão. Um flush global remove proteção de todas as chaves ao mesmo tempo.

Versionamento de namespace

v3:product:42

Ao mudar formato, use nova versão. Chaves antigas expiram naturalmente, evitando limpeza massiva.

Invalidação por evento

Quando um produto muda, publique evento para remover L1 e L2. Ainda mantenha TTL como segurança.

Consistência

Defina tolerância por recurso:

  • conteúdo editorial: minutos;
  • catálogo: segundos ou minutos;
  • estoque: curto e validado na compra;
  • saldo: não usar stale;
  • feature flags: conforme risco.

Observabilidade

Meça:

  • hit, miss e stale hit;
  • reconstruções;
  • requisições coalescidas;
  • locks adquiridos e disputados;
  • tempo de espera;
  • loader duration;
  • fallback stale-if-error;
  • fila de regeneração;
  • hot keys agregadas;
  • erros do cache.

Não use chave completa de usuário como label.

Teste de carga

Expire uma chave popular durante carga e observe quantas chamadas chegam à origem. Repita com coalescing, lock e stale. Meça p99, banco e Redis.

Testes de falha

Simule crash do lock holder, TTL do lock, Redis indisponível, origem lenta, stale expirado, múltiplos pods e invalidação simultânea.

Erros comuns

  • usar TTL igual para todas as chaves;
  • lock sem token;
  • lock sem expiração;
  • esperar indefinidamente;
  • coalescing apenas local em operação cara;
  • não fazer segundo check;
  • servir stale em dado crítico;
  • cachear exceção;
  • flush global;
  • não limitar loaders.

Fluxo recomendado

Comece com request coalescing, adicione jitter e use stale-while-revalidate. Para múltiplas réplicas e loaders caros, coordene com lock seguro. Combine com Cache HTTP, Redis com Node.js, Circuit Breaker e Rate Limiting.

Consulte a referência de Cache-Control e a documentação oficial do armazenamento usado para locks atômicos.

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