A request que ninguém olhou

Abre o DevTools (F12) do teu navegador agora. Vai na aba Network. Atualiza qualquer página de um sistema que tu mantém ou já trabalhou. Olha a coluna de Status.

Tudo 200 OK? Atualiza a página. Tudo 200 novamente? Tudo voltando o payload inteiro? Parabéns, tu tá pagando banda, CPU e tempo de resposta pra entregar exatamente a mesma coisa que o navegador já tinha na mão dois segundos atrás.

Eu já vi isso em sistema grande, sistema pequeno, sistema de startup. É quase universal. E o motivo é sempre o mesmo: ninguém ensina cache HTTP. Ninguém. O pessoal aprende Redis antes de aprender ETag. Aprende a configurar uma CDN paga antes de aprender o que o navegador já faz de graça desde 1997, 30 anos atrás.

Hoje eu quero falar do 304 Not Modified. O status code mais subestimado da web.

O problema que fingem que nao existe.

Pede pro ChatGPT, Claude, Cursor, qualquer um deles: "como otimizo a performance da minha API?".

Aposto o almoço que a resposta vai vir com Redis. Talvez Memcached. Talvez uma CDN. Talvez um background job pra "pré-computar" alguma coisa. Provavelmente vai vir complexidade. Tudo isso faz sentido, mas a pergunta é: será que é a hora?

O que não vai vir, em 9 de 10 vezes, é a pergunta certa: "teu cache HTTP tá configurado?" Esse seria a primeira pergunta que qualquer pessoa com experiência faria antes de meter Redis num sistema que processa poucas requests por minuto.

A otimização mais cara é a que tu adiciona quando a de graça já existia.

O que é 304 Not Modified, sem enrolação

Funciona assim:

  1. Navegador pede um recurso pela primeira vez. Servidor responde 200 OK com o conteúdo e um header tipo ETag: "abc123" (é só um hash do conteúdo, pode ser qualquer identificador único).
  2. Da segunda vez em diante, o navegador manda a mesma request mas com um header novo: If-None-Match: "abc123". Tipo dizendo: "eu já tenho a versão abc123, ainda serve?".
  3. Servidor compara. Se o conteúdo não mudou, responde 304 Not Modified. Sem corpo. Sem JSON. Sem HTML. Só o status.
  4. Navegador usa o que ele já tinha em cache.

Latência cai. Banda cai. O servidor ainda processa a request, mas não precisa serializar nada, não precisa renderizar nada, não precisa mandar nada pelo fio. E em muitos casos, nem precisa bater no banco.

Custo de implementação no Rails: uma linha.

def show
@post = Post.find(params[:id])
fresh_when @post
end

É isso. fresh_when calcula o ETag a partir do updated_at do objeto, manda no header, e na próxima request compara automaticamente. Se o post não foi atualizado, Rails responde 304 antes mesmo de renderizar a view.

Uma linha. Não Redis. Nem gem nova. Nem configuração. Uma linha que o framework te dá há mais de uma década e que praticamente ninguém usa.

Quer ver na prática a diferença? Vamos usar o DevTools e carregar o index desse blog.

screenshot-2026-05-17_21-05-14.png 147 KB


Vamos lá, aqui tem algumas particularidades para prestar atenção. Primeiro: 39 requests. Muitas? Não, para HTTP/2, que é o que hoje em dia é usado, definitivamente não. Segundo ponto importante e o que eu quero focar: 851 kB transferidos. Isso é pouco porque o blog é pequeno, tem poucos posts e coisas para carregar/baixar. Agora atualiza a página novamente.

screenshot-2026-05-17_21-15-15.png 167 KB

Na primeira linha, o status 304 foi onde a mágica do ETag aconteceu; antes foi baixado 6.7 kB; agora, com o servidor mandando somente o status 304 Not Modified, apenas 0.4 kB, uma economia de quase 95%. Isso foi só a primeira request. As outras requests com status "200"? Que request? Eles nunca foram feitas; foram cacheadas naquela primeira vez que carregamos a página. O servidor deu instruções para o browser fazer isso. O que se vê? Cache-Control de 1 ano.

screenshot-2026-05-17_21-29-37.png 42,9 KB


Mas aí fica a pergunta: é seguro fazer cache de um ano? Nesse cenário, com a estratégia que o Rails usa para entregar assets, total. Poderia fazer cache de 2, 3, 4 ou 10 anos. Tá vendo esse hash no final do arquivo? a9f4a8cd Isso é o fingerprint do arquivo: quando o arquivo mudar, o fingerprint vai mudar; ou seja, o cache vai ser automaticamente invalidado.

Agora, no total, em termos de banda de internet, saímos de 851 kB para 568 B, foram 99,93% de economia. Parece mágica, mas não é; é apenas o protocolo HTTP funcionando da forma como foi desenhado anos atrás.

Os três caches que ninguém distingue

Aqui tá a parte que confunde quem nunca parou pra estudar isso direito. Quando alguém fala "cache", pode ser uma de três coisas completamente diferentes, e elas não competem entre si, elas se complementam:

  1. Cache total no navegador — Cache-Control: max-age=3600. O navegador nem manda a request. Olha o relógio, vê que ainda tá dentro da validade, e usa o que tem em disco. Tempo de resposta: zero. Ideal pra assets (CSS, JS, imagens, fontes).
  2. Cache de validação — ETag + 304. A request vai até o servidor, mas se nada mudou, volta vazia. Ideal pra páginas e endpoints que mudam de vez em quando, mas não toda hora. Posts de blog, perfis, listas de produtos.
  3. Cache do lado do servidor — Redis, Memcached, Rails.cache. Tu evita a query no banco, evita o cálculo caro. Mas ainda renderiza, ainda serializa, ainda transfere pelo fio. Útil quando o gargalo é o backend, não a rede.

A maioria dos sistemas pula direto pro item 3 sem nunca ter passado pelo 1 e pelo 2.

Como isso parece num projeto Rails de verdade:

Imagina um blog (que conveniente). Um post tem updated_at. O conteúdo só muda quando eu edito. Entre edições, podem passar semanas.

Sem cache HTTP, toda visita roda:

  • Roteamento
  • Controller
  • Query no banco (Post.find)
  • Render da view (Markdown, syntax highlight, partials)
  • Serialização do HTML
  • Transferência pela rede

Com fresh_when @post, a partir da segunda visita do mesmo usuário (e dos crawlers que respeitam o cache, e dos proxies no
caminho), tudo isso vira:

  • Roteamento
  • Controller
  • Query no banco (sim, ainda roda, é o trade-off)
  • Comparação de ETag
  • Resposta 304 vazia

Já cortou metade do trabalho. E se eu quiser cortar a query também, uso stale? Com um bloco e dou um jeito de pegar só o updated_at antes de carregar o objeto inteiro. Aí vira otimização de verdade. Sem dependência externa. Sem servidor novo. Sem operação a mais.

Onde 304 não resolve

Não vou ser igual aos vendedores de ilusão e dizer que 304 é bala de prata. Não é. Há casos em que simplesmente não dá:

  • Página autenticada com conteúdo personalizado por usuário. Cuidado redobrado: tu precisa garantir que o cache não vaze dados de um usuário pro outro. Vary: Cookie, escopo certo, ETag que inclua o ID do usuário. Erro aqui é vazamento de dados, não bug de performance.
  • Conteúdo em tempo real. Dashboard com métricas que atualizam a cada segundo? Cache HTTP atrapalha mais do que ajuda.
  • POST, PUT, DELETE. Cache é pra leitura. Escrita não cacheia (e nem deve).
  • APIs onde o cliente não respeita os headers. Tu manda ETag, o cliente ignora, manda request completa toda vez. Acontece com clientes mobile mal escritos, scripts de scraping, integrações antigas.

Cache HTTP brilha em conteúdo público ou semi-público que muda de vez em quando. Que é, vamos ser sinceros, a maior parte da internet.

Fechando

O 304 Not Modified não é um truque. Não é uma técnica avançada. Não é segredo de sênior. É o protocolo funcionando como foi desenhado pra funcionar há quase 30 anos. O que é raro é alguém parar pra usar.

Se tu tá começando, antes de aprender Redis, aprende HTTP. Antes de configurar uma CDN paga, configura Cache-Control. Antes de meter background job pra "pré-computar", testa se fresh_when resolve.

E se tu já é experiente e nunca olhou pra isso com carinho: abre o DevTools agora. Vai te doer ver quanto tráfego inútil tua aplicação tá servindo.

A internet rápida não foi feita de Redis. Foi feita de gente que entendeu o protocolo.