Anúncios

    Era 23h12 de uma sexta-feira quando o celular do líder técnico de uma fintech paulistana começou a vibrar sem parar. O banco de dados principal tinha chegado a 98% de utilização de CPU, e o sistema de pagamentos — responsável por processar cerca de 40 mil transações por hora no pico — estava respondendo em mais de 8 segundos. Oito segundos. Pra uma tela de confirmação de pagamento, isso é o suficiente pra fazer o usuário desistir e ligar pra reclamar.

    O time passou a madrugada inteira tentando escalar o banco horizontalmente. Mas o problema não era a capacidade do banco. Era que eles estavam consultando os mesmos dados de configuração de produto 400 vezes por segundo — dados que mudavam, no máximo, uma vez por semana. O banco virou bode expiatório de um problema que era, na verdade, de arquitetura de acesso a dados.

    E aqui tá o ponto que a maioria das equipes ignora: o gargalo de latência raramente é onde você acha que é. A discussão quase sempre começa em “precisamos de um banco mais rápido” ou “vamos jogar mais memória no servidor”, quando na verdade o problema é que a aplicação não tem memória alguma — ela esquece tudo a cada requisição e sai perguntando a mesma coisa de novo.

    1. Cache não é só Redis na frente do banco

    A primeira coisa que vem à cabeça quando alguém fala em cache pra aplicação web é: “bota um Redis aí”. Faz sentido — o Redis é excelente, battle-tested, e tem adoção massiva. Mas cache é uma estratégia de múltiplas camadas, e tratar ele como se fosse só “um serviço que fica na frente do banco” é desperdiçar 80% do potencial.

    Pensa nas camadas que existem antes de uma requisição sequer chegar ao seu Redis:

    • Cache do cliente (browser): headers como Cache-Control, ETag e Last-Modified podem fazer o browser nem fazer a requisição. Zero latência de rede.
    • CDN e edge cache: serviços como Cloudflare ou equivalentes nacionais conseguem servir respostas inteiras de um datacenter a 20ms do usuário, sem nem bater no seu servidor de aplicação.
    • Cache na camada de aplicação (in-process): um Map ou biblioteca como Caffeine (Java) ou memory-cache (Node) que vive dentro do próprio processo. Latência de microssegundos — sem rede.
    • Cache distribuído (Redis, Memcached): o clássico. Compartilhado entre instâncias, persistível, com TTL configurável.
    • Cache de query no banco: presente em alguns bancos, mas raramente confiável como estratégia principal.

    Cada camada tem seu custo, sua granularidade e seu caso de uso ideal. Usar só uma delas é como colocar um filtro de ar na saída do carro e achar que resolveu a poluição.

    2. A estratégia de invalidação é onde tudo quebra

    Phil Karlton tinha razão quando disse que existem só duas coisas difíceis em ciência da computação: invalidação de cache e dar nomes para as coisas. Eu adicionaria uma terceira: convencer o time de que invalidação de cache precisa de design intencional antes de virar problema.

    As três estratégias mais usadas têm características bem distintas:

    TTL simples (Time-To-Live)

    Você define que o dado expira em X segundos. Simples, previsível, fácil de implementar. O problema é que você vai servir dados desatualizados por até X segundos — e em alguns contextos isso é inaceitável. Num catálogo de produtos de e-commerce, um TTL de 5 minutos geralmente tá ótimo. Num saldo bancário, zero segundos de tolerância.

    Cache-aside com invalidação explícita

    A aplicação é responsável por remover (ou atualizar) a entrada no cache quando o dado muda. Mais preciso, mas exige disciplina: toda operação de escrita precisa lembrar de invalidar o cache correspondente. Esqueceu uma? Dado velho até o TTL expirar — se tiver TTL.

    Write-through e write-behind

    O cache fica na frente da escrita, não só da leitura. No write-through, você escreve no cache e no banco simultaneamente. No write-behind, você escreve no cache e deixa a sincronização com o banco acontecer de forma assíncrona. Esse último é poderoso pra performance de escrita, mas exige cuidado redobrado com consistência — se a aplicação cair antes de sincronizar, você perdeu dados.

    Levantamentos do setor de infraestrutura mostram que a maioria dos incidentes de produção relacionados a cache não vêm de falha do servidor de cache em si, mas de lógica de invalidação incorreta ou ausente. Isso é o tipo de coisa que aparece só em sexta à noite.

    3. O padrão Stale-While-Revalidate que ninguém usa direito

    Um dos padrões mais poderosos — e menos explorados em aplicações backend — é o stale-while-revalidate. A ideia é simples: se o dado expirou mas você ainda tem ele em mãos, serve o dado velho imediatamente e dispara uma atualização em background. O usuário recebe uma resposta rápida, e na próxima requisição já vai ter o dado fresco.

    Esse padrão é nativo no HTTP (via header Cache-Control: stale-while-revalidate=60) e pode ser implementado manualmente no backend também. O resultado prático é que você elimina o problema do “cache miss frio” — aquele momento onde o cache expirou e a primeira requisição vai ter que esperar o banco inteiro antes de responder.

    Num projeto de portal de notícias que acompanhei de perto, a implementação desse padrão reduziu o p99 de latência de 1.400ms para menos de 200ms em páginas de listagem — sem mudar uma linha de código do banco de dados. O dado tinha 30 segundos de atraso máximo, o que era perfeitamente aceitável pra notícias que são atualizadas a cada 5 minutos.

    4. O que não funciona (e todo mundo continua fazendo)

    Tenho opinião forte sobre algumas abordagens que viram moda e causam mais dor do que aliviam:

    Cachear tudo com o mesmo TTL. Definir um TTL de 5 minutos pra toda a aplicação é o equivalente a colocar a mesma senha em todas as contas. Funciona até o dia que não funciona. Dados de configuração podem ter TTL de horas. Dados de sessão de usuário, minutos. Preços em promoção relâmpago, talvez zero.

    Usar cache como solução pra query mal escrita. Já vi equipe cachear o resultado de uma query que rodava em 12 segundos em vez de simplesmente adicionar um índice. O cache escondeu o sintoma, mas a query continuava lá, esperando o dia em que o cache falhasse pra derrubar tudo de novo. Cache não é curativo — é preventivo.

    Ignorar o thundering herd problem. Quando um cache popular expira e 500 requisições simultâneas chegam ao mesmo tempo, todas vão pro banco ao mesmo tempo. O resultado é um pico de carga que pode derrubar o banco mesmo com cache implementado. A solução — mutex/lock na renovação do cache, ou TTL com jitter — é simples, mas raramente lembrada.

    Não monitorar taxa de hit/miss. Cache sem observabilidade é caixa-preta. Se você não tá olhando pra hit rate por chave ou por endpoint, você não sabe se o cache tá ajudando ou só adicionando latência de rede a toa. Uma taxa de hit abaixo de 85% em dados que deveriam ser cacheáveis é sinal de problema na estratégia de chaveamento.

    5. Caso concreto: antes e depois numa API de consulta de CEP

    Pra tornar isso tangível: uma API de consulta de endereço por CEP — o tipo de coisa que aparece em todo checkout de e-commerce brasileiro. Os dados de CEP mudam raramente (novos loteamentos, correções do Correios), mas a consulta é feita milhares de vezes por dia.

    Antes: cada consulta batia direto na base de dados. Latência média de 180ms, pico de 600ms em horário de maior tráfego. Custo de banco crescendo linearmente com o número de usuários.

    Depois da implementação em camadas:

    • Cache in-process com os 10 mil CEPs mais consultados (responsáveis por ~60% do tráfego): latência de 2ms, sem rede.
    • Redis com TTL de 24 horas pra todos os outros CEPs já consultados: latência de 8ms.
    • Banco só para cache miss real (CEP nunca visto): latência original de 180ms, mas acontecendo em menos de 5% das requisições.

    O resultado não foi perfeito desde o primeiro dia. Na primeira semana, o cache in-process foi configurado sem limite de memória e cresceu até 800MB dentro de algumas horas — o que causou um episódio de GC pause no serviço Java. Depois de configurar o tamanho máximo com política de eviction LRU, estabilizou em menos de 120MB e nunca mais deu problema.

    Esse detalhe — o GC pause que não estava no roteiro — é exatamente o tipo de coisa que aparece quando você coloca cache de verdade em produção. Não é motivo pra não fazer. É motivo pra testar direito antes.

    6. Chaves de cache que não viram armadilha

    A estrutura da chave de cache importa mais do que parece. Uma chave mal definida gera ou colisão (dado de um usuário sendo servido pra outro — isso já causou vazamento de dados em produção em casos documentados) ou fragmentação excessiva (cache cheio de entradas únicas que nunca são reutilizadas).

    Boas práticas que funcionam na prática:

    • Inclua versão do dado ou da aplicação na chave quando o schema mudar com frequência: produto:v2:12345
    • Nunca use dados sensíveis diretamente na chave (CPF, email) — use um hash ou identificador interno
    • Prefixe por contexto pra facilitar invalidação em grupo: catalogo:categoria:eletronicos:*
    • Documente o padrão de chaveamento igual você documenta o schema do banco — vai agradecer em seis meses

    O próximo passo concreto que você pode dar hoje

    Não precisa refatorar a arquitetura inteira essa semana. Três coisas pequenas com alto retorno:

    1. Olhe os headers HTTP das suas respostas agora. Abra o DevTools em qualquer endpoint da sua aplicação e veja se tem Cache-Control configurado. Se a resposta for “não tem” ou “no-store em tudo”, você tá deixando a camada mais barata de cache completamente inutilizada.

    2. Identifique uma query que roda mais de 100 vezes por minuto com os mesmos parâmetros. Só uma. Instrumente o banco por 30 minutos e veja quais queries se repetem. Essa é sua primeira candidata a cache com TTL curto — mesmo 60 segundos já muda o jogo.

    3. Configure um dashboard de hit rate no seu Redis (ou equivalente). O comando INFO stats no Redis já te dá keyspace_hits e keyspace_misses. Se você não tiver isso visível hoje, você não sabe se o cache que já existe tá funcionando.

    Começa por um desses. Qualquer um. O resto aparece naturalmente quando você começa a ver os números.