No artigo anterior desta série, apresentamos os quatro padrões arquiteturais de cache, e mostramos como cada um deles resolve um problema diferente dentro do espectro de consistência e disponibilidade que o teorema CAP descreve. O cache-aside protege o banco em leituras. O write-through mantém coerência nas escritas. O write-behind absorve picos de escrita ao custo de aceitar risco de perda. Cada padrão tem seus trade-offs, e a decisão entre eles depende do tipo de dado e dos requisitos do sistema.
Mas há algo que todos os padrões têm em comum e que raramente aparece na discussão técnica inicial. É que cada um deles adiciona complexidade ao sistema, e complexidade tem custo. Esse custo é o tema deste artigo.
Quando uma equipe decide adicionar cache, o foco quase sempre está no ganho de performance. O banco de dados está lento. O Redis vai resolver. Os números de latência melhoram no próximo deploy. A história termina com sucesso. O que essa narrativa omite é o que acontece depois, quando o sistema está em produção há seis meses, a equipe que tomou a decisão cresceu, novos desenvolvedores chegaram sem o contexto original, os dados que foram escolhidos para cache mudaram de natureza, e o primeiro incidente de inconsistência bate na porta.
O custo financeiro que aparece na fatura
Começar pelo custo financeiro é útil porque ele é o mais concreto e o mais fácil de ignorar no momento da decisão. Quando uma equipe avalia se vai adicionar Redis, o pensamento natural é comparar o custo do Redis com o custo de um banco de dados maior. Esse é o cálculo errado.
O custo do Redis em produção não é o custo de uma única instância. É o custo de um sistema operado com os padrões mínimos de confiabilidade que produção exige. Uma instância isolada de Redis que falha deixa o sistema sem cache. Em alguns padrões, como vimos no artigo anterior, isso significa que todas as requisições vão direto ao banco, que pode não suportar a carga. Em outros, como o write-behind, significa que dados ainda não persistidos são perdidos.
Por isso, cache em produção significa Redis em configuração de alta disponibilidade. Isso tipicamente implica um nó primário e pelo menos um nó de réplica, com Redis Sentinel monitorando a saúde do cluster e promovendo automaticamente a réplica em caso de falha do primário. Em ambientes de nuvem, essa configuração tem custo específico. Uma instância ElastiCache for Redis de 13 GB na AWS com modo Multi-AZ habilitado, que é o mínimo razoável para um serviço de produção com alta disponibilidade, custava em torno de 340 dólares por mês na região us-east-1 em dezembro de 2025, de acordo com a calculadora de preços da AWS. Para 26 GB, o custo subia para cerca de 660 dólares mensais. Os valores exatos mudam com o tempo, mas a ordem de grandeza tende a ser estável: infraestrutura gerenciada com alta disponibilidade opera na casa das centenas de dólares mensais, não de dezenas nem de milhares, pela natureza dos custos fixos de replicação, failover e operação. Isso antes de considerar transferência de dados, snapshots, e custo de instâncias maiores se o volume crescer.
Esses números não são proibitivos para a maioria das empresas, mas precisam ser contabilizados e justificados. Quando o time de FinOps revisa a fatura de infraestrutura e encontra uma linha de Redis que não existia no trimestre anterior, a pergunta vai aparecer: o que isso está resolvendo? Qual o ROI? Quando isso foi adicionado e quem aprovou? Em empresas com processos mais maduros de gestão de custos de cloud, qualquer novo recurso de infraestrutura deveria ter uma justificativa de custo-benefício documentada desde o início.
O custo operacional que aparece depois
Além do custo financeiro direto, há o custo operacional de manter o Redis funcionando. Redis é um sistema estável, mas como qualquer componente de infraestrutura, precisa de atenção contínua.
Memória é o recurso mais crítico do Redis. Diferente de um banco de dados relacional, que pode usar disco quando a memória enche, o Redis é um sistema puramente em memória por padrão. Quando a memória configurada para o Redis se esgota, o Redis precisa decidir entre rejeitar novas escritas ou remover dados existentes para abrir espaço. Essa política é configurável através do parâmetro maxmemory-policy. As opções mais comuns são allkeys-lru, que remove os dados menos recentemente usados entre todos os dados existentes, e volatile-lru, que remove apenas dados com TTL configurado usando a mesma política.
Nenhuma dessas opções é neutra. Se o Redis começa a remover dados por pressão de memória, o hit ratio cai. Se o hit ratio cai, mais requisições vão ao banco. Se o banco não estava dimensionado para absorver esse volume adicional, a latência sobe. O sistema começa a degradar por um motivo que raramente aparece nos dashboards de forma direta, porque o processo é gradual e vem da interação entre dois componentes.
Para evitar isso, é preciso monitorar ativamente o uso de memória do Redis, a taxa de evicção, e o hit ratio. Quando o Redis começa a evictar dados com frequência, é sinal de que a memória está insuficiente para o volume atual de dados ou que o TTL está muito longo e os dados estão acumulando mais do que deveriam. Ambos os casos exigem intervenção, e qualquer intervenção tem custo de engenharia.
Versões e atualizações são outro vetor de custo operacional. O Redis lança novas versões periodicamente, com mudanças de comportamento que podem afetar aplicações. A migração de uma versão para outra em produção, especialmente com dados críticos no cache, exige planejamento, testes, e uma janela de manutenção ou uma estratégia de migração sem downtime. Em sistemas que usam comandos menos comuns do Redis, mudanças de semântica entre versões podem causar bugs silenciosos que só aparecem em condições específicas de carga.
O custo cognitivo que aparece quando alguém precisa entender o sistema
O custo menos discutido, e possivelmente o mais duradouro, é o custo cognitivo. Todo componente adicionado a um sistema aumenta o número de estados possíveis que o sistema pode assumir. Um sistema sem cache tem um estado possível, o que está no banco. Um sistema com cache tem pelo menos três estados relevantes para qualquer dado: o dado está correto no banco e correto no cache, o dado foi atualizado no banco mas o cache ainda tem a versão antiga, ou o dado não está no cache e a próxima requisição vai ao banco.
Esse aumento de estados torna o sistema mais difícil de raciocinar, especialmente para pessoas que chegaram ao time depois que o cache foi adicionado. Quando um bug aparece e o dado que o usuário vê é diferente do que está no banco, a primeira pergunta é sempre "isso é um problema de cache?". Descobrir a resposta exige instrumentação adequada, acesso a logs de ambos os sistemas, e conhecimento sobre os TTLs e padrões de invalidação em uso. Em sistemas com múltiplas camadas de cache (cache local em memória na aplicação, Redis distribuído, e CDN para conteúdo estático), o diagnóstico se torna ainda mais complexo.
Phil Karlton, engenheiro da Netscape, capturou esse custo cognitivo na frase que provavelmente é a mais citada na engenharia de software:
"Existem apenas duas coisas difíceis em ciência da computação: invalidação de cache e dar nomes às coisas."
— Phil Karlton, citado por Martin Fowler em TwoHardThings, martinfowler.com
A frase é irônica na forma mas precisa no conteúdo. Invalidação de cache é difícil porque o problema não é técnico no sentido de exigir algoritmos complexos. O problema é de coordenação. Como garantir que quando um dado muda em um lugar, todas as cópias desse dado em todos os outros lugares sejam atualizadas ou removidas de forma correta, completa, e no momento certo.
Por que a invalidação é o problema mais difícil
Para entender por que a invalidação é difícil, é preciso entender o que acontece quando ela falha. Quando o cache contém um dado desatualizado, o sistema serve uma versão incorreta da realidade para os usuários. Em sistemas de e-commerce, isso pode significar mostrar um preço antigo que já foi atualizado, o que cria expectativas que o sistema não pode honrar. Em sistemas de inventário, pode significar permitir que um usuário adicione ao carrinho um produto que já esgotou. Em sistemas financeiros, pode significar mostrar um saldo diferente do real, com consequências legais e reputacionais.
A dificuldade técnica da invalidação vem de uma assimetria constitutiva. Escrever um dado em um banco de dados e em um cache ao mesmo tempo é simples. Garantir que quando esse dado muda no banco, todas as instâncias do cache que têm uma cópia dele sejam notificadas e atualizadas, mesmo em presença de falhas de rede, falhas de processo, reinicializações de nós, e condições de corrida entre operações concorrentes, é de uma ordem de dificuldade completamente diferente.
No post Cache Made Consistent, publicado no blog de engenharia do Meta, a equipe descreve o trabalho que realizaram para melhorar a consistência do cache nos sistemas TAO e Memcache. A dimensão do problema pode ser calibrada por um número específico que o post apresenta.
"Ao longo dos anos, melhoramos a consistência do cache do TAO por uma medida, de 99,9999 por cento (seis noves) para 99,99999999 por cento (dez noves)."
— Meta Engineering Blog, Cache Made Consistent, engineering.fb.com
Para calibrar a dimensão desse número, o TAO serve mais de um quatrilhão de requisições por dia. Com consistência de seis noves, menos de uma em um milhão de escritas resultaria em inconsistência. Com o volume do TAO, isso ainda significa bilhões de inconsistências por dia em termos absolutos. Chegar a dez noves, menos de uma em dez bilhões de escritas, exigiu anos de engenharia dedicada, o desenvolvimento de um sistema de observabilidade específico chamado Polaris, e uma biblioteca de rastreamento de mutações de cache embutida em cada servidor de cache da empresa. O Meta tinha uma equipe inteira dedicada exclusivamente a esse problema.
Isso não significa que qualquer sistema precisa dessa sofisticação. Significa que cache em escala expõe um problema que tem profundidade de engenharia não intuitiva, e que subestimar essa profundidade leva a sistemas com inconsistências que aparecem de forma intermitente e são difíceis de diagnosticar.
O cache stampede como modo de falha específico
Um dos modos de falha mais documentados em sistemas com cache é o cache stampede, também chamado de thundering herd problem. Entender como ele acontece ajuda a entender por que cache pode ser a causa de um incidente em vez da solução.
O stampede ocorre quando uma chave popular no cache expira, e múltiplas requisições chegam ao sistema simultaneamente antes que qualquer uma delas consiga reconstruir a entrada no cache. Cada requisição detecta o cache miss. Cada requisição vai ao banco de dados para buscar o dado. Cada requisição armazena o resultado no cache. O banco recebe um volume de requisições idênticas equivalente ao número de clientes que chegaram no intervalo entre a expiração da chave e a primeira reconstrução bem-sucedida.
sequenceDiagram
participant R1 as Requisição 1
participant R2 as Requisição 2
participant R3 as Requisição 3..N
participant C as Cache
participant DB as Banco de Dados
Note over C: Chave popular expira (TTL = 0)
R1->>C: GET produto:popular
R2->>C: GET produto:popular
R3->>C: GET produto:popular
C-->>R1: nil (miss)
C-->>R2: nil (miss)
C-->>R3: nil (miss)
R1->>DB: SELECT ... (query cara)
R2->>DB: SELECT ... (query cara)
R3->>DB: SELECT ... (query cara)
Note over DB: Sobrecarga por N queries simultâneasPara uma chave que recebia 10.000 requisições por segundo, a expiração do TTL pode significar 10.000 queries chegando ao banco no mesmo segundo. Se essa query leva 200 milissegundos para ser respondida, o banco recebe 2.000 queries simultâneas em vez das poucas dezenas que normalmente recebia para esse dado. O banco entra em contenção de recursos. A latência sobe. O sistema degrada exatamente no ponto onde o cache deveria estar protegendo-o.
O Facebook descreveu o problema em detalhes no artigo sobre o Memcache apresentado na USENIX NSDI em 2013. A solução que a equipe desenvolveu foi um mecanismo chamado lease: um token distribuído pela camada de cache para controlar qual cliente tinha permissão de reconstruir uma chave expirada enquanto os demais aguardavam ou recebiam um valor levemente desatualizado. Na escala do Facebook, uma única chave popular expirada gerava dezenas de milhares de requisições simultâneas ao banco, tornando o controle de quem reconstrói o cache tão importante quanto o cache em si.
"Um memcached server expede um lease para um cliente para setar os dados no cache quando ocorre um cache miss. [...] Com o lease, um cliente pode detectar que o valor que ele está prestes a setar já foi invalidado por uma atualização subsequente."
— Rajesh Nishtala et al., Scaling Memcache at Facebook, USENIX NSDI, 2013 (tradução nossa)
O cenário descrito até aqui, uma única chave popular expirando e atraindo um volume súbito de requisições, é a forma mais direta do problema. Há um cenário mais severo que aparece em sistemas com múltiplos tipos de dado em cache. Mesmo com TTLs distintos entre tipos de chaves diferentes, os ciclos de criação e os ritmos de atualização do sistema podem fazer com que várias expirações se concentrem em uma janela de tempo muito curta. Quando isso ocorre, cada tipo de dado que expira gera seu próprio grupo de requisições simultâneas ao banco. O efeito é multiplicativo. O banco, que antes absorvia o impacto de uma única chave amplificada, passa a absorver o impacto de múltiplos tipos de chaves amplificados ao mesmo tempo.
Esse padrão se manifesta com mais intensidade em deploys que limpam o cache e forçam repovoamento completo, em falhas de Redis seguidas de recuperação, e em sistemas com ciclos de processamento periódico que escrevem no cache em rajadas. Em cada um desses contextos, um grande volume de entradas é criado em um intervalo curto, aumentando a probabilidade de que expirações de tipos diferentes se sobreponham nos ciclos seguintes. O problema, que parecia ser sobre uma chave popular, acaba sendo sobre o ritmo em que o cache foi construído.
Quatro abordagens dominam a literatura sobre o problema. O single flight garante que, quando múltiplas requisições chegam simultaneamente para a mesma chave ausente, apenas uma delas vai ao banco enquanto as demais ficam suspensas aguardando o resultado da primeira. Quando a requisição que foi ao banco retorna, todas as demais recebem o mesmo valor sem ter gerado queries adicionais. A implementação mais citada é o pacote singleflight da biblioteca estendida do Go. A vantagem em relação ao lock distribuído é que o single flight opera inteiramente dentro do processo. Não requer componente externo e não tem problema de liveness se o processo morrer durante a reconstrução. A limitação é simétrica. Ele protege apenas dentro de uma instância. Em um serviço com dez instâncias horizontais, cada instância individualmente não vai ao banco mais de uma vez por chave ausente, mas o banco ainda pode receber dez queries simultâneas, uma por instância. O mecanismo de lease descrito pelo Facebook no paper do Memcache é, em essência, uma versão distribuída desse mesmo padrão. A coordenação que o single flight faz em memória dentro de um processo, o lease faz via tokens entre processos distintos. O lock distribuído garante que apenas uma requisição reconstrua o cache enquanto as demais aguardam ou recebem um valor desatualizado, mas desloca o problema para outra dimensão. Se o processo que adquiriu o lock morrer antes de concluir a reconstrução, o sistema precisa de um mecanismo externo de liveness para liberá-lo e permitir que outra requisição assuma. Isso adiciona um componente operacional ao sistema que o lock por si só não resolve. O TTL com jitter distribui as expirações no tempo ao adicionar aleatoriedade ao valor do TTL no momento da criação, diluindo o pico em vez de eliminá-lo. A eficácia depende do intervalo de aleatoriedade ser proporcional ao volume de chaves. Um intervalo calibrado para determinado volume pode ser insuficiente se o sistema crescer. O probabilistic early expiration, descrito por Andrea Vattani, Flavio Chierichetti e Kedar Dhamdhere na VLDB Conference em 2015, recalcula o dado antes de expirar com uma probabilidade que cresce à medida que o TTL se aproxima de zero, evitando que o cache chegue ao miss. A condição implícita é que o custo de recálculo seja previsível e baixo. Em dados com computação cara, essa abordagem pode trocar um stampede periódico por degradação contínua de fundo. O padrão que atravessa os quatro casos é que a complexidade não desaparece, ela se desloca para uma dimensão diferente do sistema.
O custo de memória e o que acontece quando ela acaba
Redis mantém todos os dados em memória. Isso é o que o torna rápido, e é também o que torna o planejamento de capacidade não trivial.
Em bancos de dados relacionais, quando os dados crescem além da memória disponível, o banco começa a usar disco para as páginas menos acessadas. A performance degrada, mas o sistema continua funcionando. Em Redis, quando os dados ultrapassam o limite de memória configurado, o Redis começa a executar a política de evicção definida. Se a política é noeviction, novas escritas são rejeitadas com erro. Se a política é allkeys-lru, dados que o sistema ainda considera válidos e úteis podem ser removidos para dar espaço a dados mais recentes.
O problema é que a pressão de memória no Redis raramente é previsível com precisão. Dados têm tamanhos variáveis. TTLs diferentes significam que diferentes quantidades de dados expiram em diferentes momentos. Picos de tráfego fazem com que mais dados sejam escritos no cache em menos tempo. A fragmentação de memória interna do Redis, um fenômeno onde a memória alocada pelo sistema para o Redis é maior do que a memória que o Redis reporta estar usando, pode reduzir efetivamente a capacidade disponível em 10 a 30 por cento em workloads com muitas escritas e deleções, conforme descrito na documentação oficial do Redis sobre otimização de memória.
Dimensionar corretamente a memória do Redis exige conhecimento do padrão de acesso, do tamanho médio dos dados armazenados, da taxa de crescimento esperada, e de uma margem de segurança que leve em conta fragmentação e picos. Esse exercício requer dados que não existem antes do sistema estar em produção, o que cria uma situação onde as primeiras semanas de um Redis recém-implantado exigem monitoramento ativo e possíveis ajustes de configuração.
Complexidade acidental e o custo de debugar
A distinção entre complexidade essencial de um sistema, aquela que decorre do problema que ele resolve, e complexidade acidental, aquela que decorre das escolhas de implementação, é a lente correta para entender o que o cache adiciona. A complexidade que o cache introduz, invalidação, stampede, pressão de memória, serve a um problema de performance que nasce das escolhas de implementação, não ao problema de negócio que o sistema existe para resolver. É acidental no sentido técnico do termo, pois existe porque escolhemos essa solução, não porque o problema exige.
Essa complexidade acidental se manifesta de formas específicas no cotidiano de desenvolvimento. Quando um desenvolvedor corrige um bug de lógica de negócio e implanta a correção, a correção pode não aparecer imediatamente para todos os usuários se o dado afetado ainda está em cache com a lógica antiga. Isso cria uma classe de bugs que só ocorrem em produção, que desaparecem após alguns minutos ou após um flush manual do cache, e que são difíceis de reproduzir em ambientes de desenvolvimento onde o cache muitas vezes não está configurado da mesma forma que produção.
Em sistemas de testes automatizados, o cache precisa ser gerenciado entre testes para garantir que um teste não veja dados deixados por um teste anterior. Isso exige que os testes limpem o cache antes ou depois de executar, o que adiciona complexidade à configuração do ambiente de testes e pode tornar os testes mais lentos se o flush do cache for uma operação cara.
Em deploys, quando uma mudança de formato de dados é feita (por exemplo, um campo é renomeado ou um novo campo obrigatório é adicionado ao objeto que é armazenado em cache), o cache pode conter objetos no formato antigo que a nova versão do código não consegue deserializar corretamente. Esse tipo de problema pode causar erros em produção imediatamente após um deploy, mesmo que os testes tenham passado, porque os testes não testaram o comportamento com dados no formato antigo que já estavam em cache.
Esse conjunto de problemas exige que a equipe pense sobre cache não apenas no momento da adição, mas como uma dimensão permanente do desenvolvimento de features. Qualquer mudança de modelo de dados precisa considerar o que acontece com os dados já em cache. Qualquer nova query que pode ser cacheada precisa considerar os padrões de invalidação. Qualquer bug fix precisa considerar se o cache pode estar mascarando ou propagando o bug.
O custo acumulado e a decisão consciente
Todos esses custos, financeiros, operacionais, e cognitivos, existem em graus variáveis dependendo do sistema e da forma como o cache foi implementado. Um cache bem projetado, com estratégias de invalidação claras, monitoramento adequado, e documentação acessível, tem custos gerenciáveis. Um cache adicionado às pressas em resposta a um incidente, sem estratégia de invalidação definida, sem monitoramento, e sem documentação, tende a acumular dívida técnica que aparece de formas inesperadas meses depois.
A decisão de adicionar cache é sólida quando cinco perguntas podem ser respondidas antes da implementação. O gargalo está mensurável na camada de dados? Cache não resolve latência de lógica de negócio, de serialização ou de rede, e adicioná-lo a um sistema com o gargalo em outra camada apenas mascara o diagnóstico. A proporção leitura-escrita do dado justifica o padrão arquitetural escolhido? Dados com alta taxa de escrita tornam qualquer estratégia de invalidação cara, e chegar a esse número exige medição, não suposição. A janela de staleness aceitável pode ser definida antes de escolher o TTL? Sem esse número, o TTL vira um chute, e em dados que afetam decisões financeiras ou de inventário, a resposta a essa pergunta frequentemente encerra a discussão antes que ela comece. As regras de invalidação podem ser enunciadas de forma explícita pela equipe? Se não for possível descrever de forma que qualquer membro do time chegue à mesma resposta quando uma entrada deve ser removida do cache, o sistema vai acumular as inconsistências intermitentes descritas neste capítulo. Existe acordo prévio sobre o que representa um hit ratio insatisfatório, em números, e sobre quem age quando esse limiar é atingido? Sem esse acordo, a taxa de evicção pode subir por semanas antes de alguém reconhecer o sintoma.
Se qualquer uma dessas perguntas não tiver resposta antes da implementação, os custos descritos neste capítulo se materializam antes do benefício que motivou a decisão.
Tratar o cache como uma decisão arquitetural com consequências duradouras, e não como uma otimização tática que pode ser adicionada e esquecida, muda o que a equipe monitora, documenta, e revisa em cada ciclo de desenvolvimento. Redis é uma ferramenta sólida. Os custos que descrevemos aqui vêm do que acontece quando a tecnologia é adicionada sem o entendimento do que se está operando.
O problema de invalidação, que estrutura todos os outros custos descritos aqui, é o tema do próximo artigo. TTL fixo e dinâmico, invalidação ativa versus passiva, versionamento de chaves, stale-while-revalidate, e as RFCs de HTTP caching que tentaram padronizar como esse problema deve ser tratado na camada de protocolo. O que o Meta levou anos para resolver no TAO e o que a comunidade de protocolos web tentou resolver nas RFCs são dois lados do mesmo problema, e a frase de Phil Karlton vai fazer mais sentido depois que entendermos por que a invalidação é difícil do ponto de vista técnico, e não apenas do ponto de vista cognitivo.