Se você já otimizou uma consulta de banco de dados, colocou um CDN na frente do seu site ou simplesmente reparou que a segunda vez que abre uma página ela carrega quase instantânea, você já esbarrou em cache. É um daqueles conceitos que todo mundo acha que entende até precisar explicar numa entrevista técnica ou debugar por que os usuários estão vendo dados velhos em produção. Aí a coisa fica interessante.
Neste post a gente vai destrinchar o cache de verdade: o que é, como funciona por baixo dos panos, os tipos que existem, as estratégias de leitura e escrita que você vai encontrar no mundo real (cache-aside, write-through, write-back) e o problema mais chato de todos, que é invalidação. Vou trazer exemplos concretos, um pouco de código e as opiniões que formei depois de tomar uns bons perrengues com Redis e headers HTTP mal configurados. A ideia é que você saia daqui sabendo não só o conceito, mas quando e como aplicar cada coisa.
O que é cache: a definição direta
Cache é uma camada de armazenamento temporário que guarda cópias de dados frequentemente acessados em um local de acesso rápido, para que requisições futuras por esses dados sejam servidas mais depressa, sem precisar recalcular ou buscar tudo de novo na fonte original. Simples assim. A fonte original pode ser um banco de dados lento, uma API externa cara, um cálculo pesado ou um disco distante. O cache fica no meio do caminho e responde primeiro quando já tem o dado guardado.
A palavra vem do francês “cacher”, que quer dizer esconder. E faz sentido: o cache é uma memória escondida, quase invisível pro usuário, que existe só pra deixar as coisas rápidas. Quando você pede um dado e ele está no cache, chamamos isso de cache hit (acerto). Quando não está e precisa ir buscar na fonte, é um cache miss (falha). A métrica que mede quão bem seu cache trabalha é a hit ratio, ou taxa de acerto: quanto mais alta, menos você incomoda o banco lá atrás.
Como o cache funciona na prática
A mecânica básica do cache gira em torno de uma pergunta que a aplicação faz antes de qualquer coisa pesada: “eu já tenho isso guardado?”. Se sim, devolve na hora. Se não, vai buscar na fonte, guarda a cópia pra próxima vez e devolve. Esse ciclo de olhar primeiro no cache, e só depois cair na fonte, é o coração de tudo.
Só que aqui aparece a primeira tensão do cache: espaço. Memória rápida é cara e limitada. Você não guarda tudo pra sempre, então precisa decidir o que fica e o que sai. É aí que entram as políticas de despejo (eviction policies). As mais comuns são LRU (Least Recently Used, descarta o que faz mais tempo que não é usado), LFU (Least Frequently Used, descarta o menos acessado) e FIFO (o primeiro que entrou é o primeiro a sair). Na maioria dos casos reais o LRU é a escolha padrão porque bate bem com o comportamento típico de acesso: dado usado recentemente tende a ser usado de novo.
O outro mecanismo central é o TTL (Time To Live), o tempo de vida. Você diz ao cache “guarda esse dado por 60 segundos” e, passado esse tempo, a entrada expira sozinha. TTL é a ferramenta mais simples e mais usada pra evitar que dados fiquem obsoletos eternamente. Não resolve tudo, como a gente vai ver na parte de invalidação, mas resolve muita coisa com esforço quase zero.

Localidade: por que cache funciona
Cache não seria útil se o acesso aos dados fosse totalmente aleatório. Ele funciona porque programas têm localidade de referência. Existe a localidade temporal (se você acessou um dado agora, provavelmente vai acessar de novo em breve) e a localidade espacial (se acessou uma posição de memória, provavelmente vai acessar as vizinhas). É essa previsibilidade que faz um cache com poucos megabytes servir a maioria das requisições de um banco com terabytes. Sem localidade, o cache viraria peso morto com hit ratio perto de zero.
Tipos e camadas de cache
Cache não é uma coisa só. Ele aparece em praticamente todas as camadas de um sistema, do hardware até a borda da internet. Vale entender onde cada um mora porque isso muda bastante como você raciocina sobre performance. Um dado pode estar em cache em cinco lugares diferentes ao mesmo tempo, e cada camada tem suas regras.
| Tipo de cache | Onde fica | O que guarda | Exemplo |
|---|---|---|---|
| Cache de CPU (L1/L2/L3) | Dentro do processador | Instruções e dados quentes | Gerenciado pelo hardware |
| Cache de aplicação (in-memory) | Na memória do processo | Objetos, resultados de cálculo | HashMap local, Caffeine |
| Cache distribuído | Servidor separado na rede | Sessões, dados compartilhados | Redis, Memcached |
| Cache de banco de dados | Dentro do SGBD | Query results, buffer pool | Query cache, buffer pool do Postgres |
| Cache HTTP / navegador | No cliente | HTML, CSS, imagens, JSON | Cache do Chrome |
| CDN / cache de borda | Servidores próximos do usuário | Assets estáticos, páginas | Cloudflare, CloudFront |
Na minha experiência, a distinção que mais importa no dia a dia de quem constrói aplicação web é entre cache local (in-memory) e cache distribuído. O cache local vive dentro do seu processo, é rápido demais porque não passa pela rede, mas some quando a instância reinicia e não é compartilhado entre servidores. Se você roda três instâncias da sua API atrás de um load balancer, cada uma tem o próprio cache local, e eles podem discordar entre si. Já o cache distribuído, tipo Redis, fica num servidor separado, todo mundo enxerga a mesma coisa, mas você paga o custo de uma ida na rede a cada consulta.
Redis: o cavalo de batalha do cache distribuído
Vale um parágrafo só pra ele porque virou quase sinônimo de cache distribuído. Redis é um armazenamento chave-valor em memória, absurdamente rápido, que aguenta centenas de milhares de operações por segundo com latência na casa dos microssegundos a poucos milissegundos. Ele não guarda só strings: tem estruturas de dados nativas como listas, sets, hashes e sorted sets, o que abre um leque grande de usos além do cache puro (filas, rankings, rate limiting, locks distribuídos). Pra cache, o comando que você mais vai usar grava a chave com um valor e um TTL embutido de uma vez só. É direto, é confiável e é o padrão da indústria por boas razões.
Estratégias de escrita e leitura
Aqui está a parte que separa quem só ouviu falar de cache de quem realmente usou em produção. Existem padrões consolidados pra decidir como o cache e o banco conversam, tanto na hora de ler quanto na hora de escrever. Escolher o padrão errado gera dados inconsistentes ou performance ruim, então vale entender cada um com cuidado.
Cache-aside (lazy loading)
Esse é de longe o mais comum, e provavelmente o que você vai implementar 90% das vezes. A lógica fica toda na aplicação: você tenta ler do cache primeiro; se der miss, busca no banco, escreve no cache e devolve. O cache é preenchido “preguiçosamente”, só quando alguém pede o dado. Por isso o nome lazy loading. Ele é ótimo porque só guarda o que realmente é usado e é resiliente: se o cache cair, a aplicação continua funcionando, só fica mais lenta.
function getUsuario(id):
dado = cache.get("user:" + id)
if dado is not null:
return dado
dado = banco.query("SELECT * FROM usuarios WHERE id = ?", id)
cache.set("user:" + id, dado, ttl=300)
return dado
O ponto fraco do cache-aside é que a primeira requisição de cada dado sempre é lenta (o miss inicial) e existe uma janela pequena onde o cache e o banco podem ficar dessincronizados se alguém escrever no banco por fora. Mas pra maioria dos sistemas isso é totalmente aceitável.
Write-through
No write-through, toda escrita passa pelo cache antes de chegar no banco. Você grava no cache e ele grava no banco de forma síncrona, na mesma operação. A vantagem é que o cache está sempre atualizado, nunca serve dado velho depois de uma escrita. O preço é que toda operação de escrita fica mais lenta, porque você paga dois writes em vez de um. Faz sentido quando as leituras são muito mais frequentes que as escritas e você não tolera inconsistência.

Write-back (write-behind)
O write-back é o mais agressivo em performance e o mais perigoso. Você grava só no cache e responde na hora pro cliente, e a escrita no banco acontece depois, de forma assíncrona, em lote. As escritas ficam rapidíssimas porque o usuário nem espera o banco. O problema é óbvio: se o cache cair antes de descarregar os dados no banco, você perde escritas. É uma estratégia para cenários de altíssimo volume de escrita onde perder algumas operações em caso de falha é aceitável, ou onde você tem mecanismos de persistência no próprio cache pra mitigar o risco.
| Estratégia | Consistência | Velocidade de escrita | Risco de perda | Quando usar |
|---|---|---|---|---|
| Cache-aside | Eventual | Normal | Baixo | Uso geral, leitura pesada |
| Write-through | Forte | Lenta | Baixo | Não tolera dado velho |
| Write-back | Eventual | Muito rápida | Alto | Volume enorme de escrita |
Existe ainda o read-through, que é parecido com o cache-aside mas com a diferença de que a própria biblioteca de cache cuida de buscar no banco quando dá miss, escondendo essa lógica da aplicação. E o write-around, onde a escrita vai direto pro banco pulando o cache, útil quando você grava dados que provavelmente não vão ser lidos tão cedo e não quer poluir o cache com eles.
O problema da invalidação de cache
Tem uma frase famosa atribuída ao Phil Karlton que diz que existem só duas coisas difíceis em ciência da computação: invalidação de cache e dar nome pras coisas. É brincadeira, mas tem muito fundo de verdade. Invalidação é o ato de remover ou marcar como inválida uma entrada do cache quando o dado por trás dela mudou. Parece trivial, mas é onde nascem os bugs mais traiçoeiros, aqueles que só aparecem em produção com carga real.
O cenário clássico: um usuário atualiza o perfil, o banco muda, mas o cache continua servindo a versão antiga porque ninguém avisou que ela ficou obsoleta. O usuário vê o dado velho, fica confuso, abre um chamado, e você passa a tarde caçando um bug que não é bug, é cache sujo. Quanto mais camadas de cache seu sistema tem (aplicação, Redis, CDN, navegador), mais lugares podem estar servindo a versão desatualizada ao mesmo tempo.
Na prática existem três abordagens pra lidar com isso. A primeira e mais simples é o TTL: você não invalida nada explicitamente, só deixa a entrada expirar sozinha depois de um tempo. É fácil, mas aceita que o dado fique errado por até o tempo do TTL. A segunda é a invalidação ativa: quando você escreve no banco, você também apaga (ou atualiza) a entrada correspondente no cache, na mesma operação. É mais correto, mas exige que você lembre de fazer isso em todo lugar que altera o dado, e é fácil esquecer. A terceira é o versionamento de chaves: em vez de invalidar, você muda a chave (por exemplo, incluindo um número de versão ou timestamp), então a chave antiga simplesmente deixa de ser consultada e some sozinha pelo eviction.
Minha opinião honesta, formada na marra: comece sempre com TTL curto. É subestimado o quanto um TTL de poucos minutos resolve na vida real sem você precisar escrever nenhuma lógica de invalidação complicada. Só parta pra invalidação ativa quando o TTL não for suficiente, tipo dados que precisam refletir mudanças imediatamente. Invalidação ativa mal feita causa mais dor de cabeça do que o TTL que ela tentou substituir.
Cache stampede: o efeito manada
Um problema relacionado que vale mencionar é o cache stampede (ou thundering herd). Acontece quando uma entrada popular expira e, no mesmo instante, centenas de requisições dão miss ao mesmo tempo e correm todas juntas pro banco pra recalcular o mesmo dado. O banco, que estava tranquilo, leva uma paulada de carga simultânea e pode cair. A solução mais usada é um lock ou “mutex” que deixa só a primeira requisição recalcular enquanto as outras esperam ou recebem o valor antigo por um instante. Vale ter isso no radar quando você trabalha com dados quentes e TTL curto.
Cache em APIs e no protocolo HTTP
Quem constrói API precisa conhecer o cache de HTTP porque ele é gratuito e poderosíssimo, e mesmo assim muita gente ignora. O protocolo HTTP tem um sistema de cache embutido, controlado por headers, que faz o navegador do usuário e os intermediários (proxies, CDNs) guardarem respostas sem você precisar de infraestrutura nenhuma. Você só precisa mandar os cabeçalhos certos.
O header mais importante é o Cache-Control. Com ele você diz por quanto tempo a resposta pode ser guardada e quem pode guardar. Um exemplo típico pra um asset estático que quase nunca muda:
Cache-Control: public, max-age=31536000, immutable
ETag: "a1b2c3d4"
O ETag merece atenção. Ele é uma espécie de impressão digital do conteúdo. Quando o cache expira, em vez de baixar tudo de novo, o navegador manda a ETag que ele tem e pergunta ao servidor “ainda vale?”. Se o conteúdo não mudou, o servidor responde com um 304 Not Modified vazio, sem reenviar o corpo. Isso economiza banda de um jeito absurdo em respostas grandes. É o mesmo princípio do Last-Modified, só que baseado em hash em vez de data.
Para dados dinâmicos de API, a lógica muda. Você não quer guardar por um ano a resposta de um endpoint que retorna o saldo da conta do usuário. Aí você usa valores curtos, ou no-cache (que força revalidação a cada uso), ou private (que permite só o navegador do usuário guardar, nunca um CDN compartilhado, porque o dado é pessoal). Um erro comum e perigoso é deixar dados privados de um usuário serem cacheados por um proxy compartilhado, o que faz um usuário ver dados de outro. Se o dado é pessoal, marque como private sem pensar duas vezes.
No nível da aplicação, também dá pra cachear respostas de API do lado do servidor, geralmente com Redis na frente do endpoint. A estratégia costuma ser cache-aside: a rota olha no Redis pela chave da requisição, se tiver serve, se não tiver processa, guarda e devolve. Para APIs que fazem consultas pesadas ou chamam serviços externos lentos, uma camada de cache assim derruba o tempo de resposta de centenas de milissegundos pra poucos, e ainda protege os serviços de trás de excesso de carga.
Perguntas frequentes sobre cache
Qual a diferença entre cache e memória RAM?
São coisas diferentes que às vezes usam o mesmo meio físico. RAM é a memória principal do computador, onde os programas rodam. Cache é um conceito, uma estratégia de guardar cópias de dados pra acesso rápido, e essa cópia pode morar na RAM (cache de aplicação, Redis), num chip dentro da CPU (cache L1/L2) ou até em disco. Então cache não é um lugar, é uma técnica, enquanto RAM é um componente de hardware específico.
Cache sempre deixa a aplicação mais rápida?
Não, e essa é uma armadilha comum. Cache adiciona complexidade e um custo fixo de verificação. Se seu hit ratio é baixo, ou seja, se você quase sempre dá miss, você está pagando o custo de olhar no cache e mesmo assim indo no banco toda vez, o que fica mais lento do que não ter cache nenhum. Cache só compensa quando os dados são lidos com muito mais frequência do que mudam, e quando o acesso tem boa localidade. Cachear tudo cegamente é uma forma clássica de piorar o sistema.
Quanto tempo de TTL eu devo usar?
Depende de quanto de desatualização você tolera. Dados que quase nunca mudam (foto de perfil, configurações) aguentam TTL de horas ou dias. Dados que mudam com frequência mas onde um pequeno atraso é aceitável (contagem de curtidas, feed) ficam bem com segundos ou poucos minutos. Dados que precisam estar sempre corretos (saldo financeiro, estoque na hora de fechar a compra) ou não devem ser cacheados, ou exigem invalidação ativa em vez de depender só de TTL. A regra prática: comece com TTL curto e vá aumentando conforme ganha confiança.
Redis ou Memcached, qual escolher?
Memcached é mais simples e faz só cache de chave-valor em memória, e faz muito bem. Redis é mais completo: tem estruturas de dados ricas, persistência opcional em disco, replicação, pub/sub e muito mais. Na prática a maioria dos times hoje escolhe Redis por default, porque ele faz tudo que o Memcached faz e ainda cobre casos futuros sem trocar de tecnologia. Só ficaria com Memcached se você tem um caso ultra específico de cache puro e quer a máxima simplicidade possível.
Como saber se meu cache está funcionando bem?
A métrica número um é a hit ratio. Se de cada 100 requisições ao cache 90 são hits, você tem 90% de hit ratio, o que geralmente é ótimo. Abaixo de uns 70% ou 80% vale investigar: talvez seu TTL esteja curto demais, talvez você esteja cacheando dados que ninguém relê, talvez o cache seja pequeno demais e o eviction esteja jogando fora coisa útil. Redis e a maioria das ferramentas expõem essa métrica direto, então monitore ela junto com latência e uso de memória.
Cache invalidation é realmente tão difícil quanto dizem?
É difícil não pela mecânica, apagar uma chave é trivial, mas por saber exatamente quando e onde invalidar em um sistema com múltiplas camadas e escritas vindo de vários lugares. O difícil é a coordenação e a completude: garantir que toda escrita, em todo caminho de código, dispare a invalidação de todas as cópias em todas as camadas. É por isso que muitos engenheiros experientes preferem TTL, que troca a complexidade da coordenação por uma tolerância controlada a dados um pouco velhos.
Conclusão
Cache é uma dessas ferramentas que parecem simples na superfície e revelam profundidade conforme você aperta. No fundo é sempre o mesmo trade-off: você troca uma pontinha de precisão (o risco de servir dado ligeiramente velho) por um ganho enorme de velocidade e uma queda grande de carga na fonte. Entender esse trade-off é entender cache de verdade.
Se eu tivesse que resumir em conselhos práticos: comece com cache-aside e TTL curto, que resolvem a esmagadora maioria dos casos com pouquíssimo código. Meça sua hit ratio antes de sair otimizando no escuro. Aproveite o cache de HTTP, que é de graça e muita gente deixa na mesa. E trate invalidação com respeito, preferindo expiração por tempo sempre que a precisão absoluta não for obrigatória. Cache bem feito é invisível: o usuário só sente que o sistema é rápido, e é exatamente esse o objetivo.
Para quem constrói sistemas que precisam de dados oficiais, o Hub do Desenvolvedor mantém APIs de consulta de CNPJ e CPF na Receita Federal prontas para você integrar sem burocracia.


