pular para o conteúdo

dia 272 da operação / 30.09.2026

Proxfrito

← sério

sério14/09/20264 min

Três perguntas antes de colocar cache em qualquer coisa

Cache não é otimização, é um contrato de tolerância a dado desatualizado. Se você não sabe escrever esse contrato, você não sabe o que está colocando em produção.

  • cache
  • performance
  • backend
  • redis

"Cache é sempre a resposta". É a frase mais repetida e menos útil da engenharia de software, porque a segunda metade da frase é sempre omitida: cache é a resposta para o problema de ler a mesma coisa muitas vezes em pouco tempo. Para qualquer outro problema ele é a resposta errada — inclusive para o problema de "o sistema está lento", que é onde ele costuma ser aplicado.

Existem três perguntas que separam um cache que funciona de um incidente com atraso de 48 horas. Se você não consegue responder as três por escrito, não coloque o cache.

1. Quanto tempo de dado desatualizado é aceitável?

Todo cache entrega dado velho em algum momento. Isso não é efeito colateral, é a função. Então a primeira pergunta não é "como invalidar", é quanto.

  • Zero segundos. Aí você não quer cache: você quer uma cópia sincronizada, uma leitura com consistência forte, ou aceitar a latência do banco.
  • Alguns segundos. Típico de listagem pública, contador, catálogo. Um Cache-Control: max-age=30 resolve e não exige uma linha de infraestrutura.
  • Minutos ou horas. Bom para dado que muda por publicação humana, como texto de marketing ou configuração.
  • Até a próxima escrita. É o caso mais duro: exige invalidação explícita, e invalidação explícita é onde quase todo bug de cache nasce.

Esse número tem nome no resto do mundo: staleness budget. Se o time não consegue dizer se o requisito é dois ou trinta segundos, é porque ninguém perguntou ao produto — e a resposta que você vai receber depois do incidente é "não pode ficar desatualizado nem por um segundo".

2. Quem invalida, e quando?

Essa é a pergunta que mata projeto. Existem três respostas honestas:

Ninguém invalida: expira pelo tempo. O cache carrega um TTL e vive com isso. Funciona bem quando o dado é tolerante. Custa pouco. É subutilizado porque parece "pouco profissional", o que é uma bobagem: metade dos sistemas de conteúdo do mundo só precisa disso.

O caminho de escrita invalida. A transação que altera o dado também derruba a chave. Aqui nasce a maior parte dos bugs, porque invalidação é fácil de esquecer em um caminho secundário:

// easy to miss: a segunda escrita NÃO invalida o cache
await db.produto.update({ where: { id }, data: preco });
await redis.del(`produto:${id}`);

// o importador em lote, esse carinha aqui, alterou 4.000 produtos e não avisou ninguém

O cache tem versão. A chave inclui a versão do dado (produto:42:v17) e a escrita promove uma versão nova. Custa mais memória, e em troca elimina a categoria inteira de "esqueci de invalidar". Se o seu dado é escrito por vários processos, esse padrão paga o investimento em uma semana.

Minha opinião marcada como opinião: se você não consegue apontar, no código, cada lugar que invalida a chave, escolha TTL e aumente a tolerância — ou escolha versão. Invalidação espalhada por memória humana é dívida técnica com juros compostos.

3. O que acontece na primeira requisição depois de expirar?

Aqui mora o incidente que todo mundo conhece e poucos prevêem. Chave expira às 9h. Às 9h00min00s, quinhentas requisições simultâneas descobrem o cache vazio e vão todas ao banco. O banco cai — não pela carga média, mas pela carga simultânea.

Duas defesas resolvem quase tudo:

Single-flight local. Só uma requisição por processo vai ao banco; as outras esperam por ela:

let emVoo: Promise<Produto> | null = null;

async function comSingleFlight(id: string): Promise<Produto> {
  if (!emVoo) {
    emVoo = buscarProduto(id).finally(() => {
      emVoo = null;
    });
  }
  return emVoo;
}

TTL com jitter. Nunca expire tudo junto. Some um ruído ao TTL (ttl + aleatório(0, ttl * 0.1)) e a expiração de vinte mil chaves deixa de ser um evento único.

const ttl = 300 + Math.floor(Math.random() * 30); // 300–330s

E o complemento óbvio: quando o backend está fora do ar, o cache stale é melhor do que nada. stale-while-revalidate e stale-if-error fazem isso no HTTP de graça, sem uma linha de código:

Cache-Control: public, max-age=60, stale-while-revalidate=300, stale-if-error=86400

O resumo que cabe num post-it

  • Cache é contrato de tolerância, não otimização.
  • Sem número de staleness acordado com o produto, não existe cache — existe aposta.
  • Se ninguém sabe quem invalida, escolha TTL ou versão. Não escolha memória.
  • Meça antes e depois. Cache que ninguém mediu é cache que ninguém pode remover.

E se depois de tudo isso o sistema continuar lento, provavelmente o problema nunca foi leitura repetida — era consulta sem índice. Mas isso é assunto do próximo texto.