Cache-Control e stale-while-revalidate em 2026: Guia Completo de Cache HTTP e Edge Caching
Guia completo de Cache-Control HTTP em 2026: max-age, s-maxage, immutable, stale-while-revalidate, ETag, invalidação por tag e políticas prontas por tipo de recurso para reduzir TTFB no edge.
Cache-Control é o cabeçalho HTTP que instrui browsers e CDNs sobre como armazenar, revalidar e servir uma resposta. Em 2026, é a alavanca mais barata que você tem para melhorar TTFB, LCP e custo de infraestrutura ao mesmo tempo. Combinado com stale-while-revalidate e uma boa camada de edge caching, dá para servir HTML dinâmico com latência de estático. Este guia cobre as diretivas essenciais, estratégias de invalidação, ETag/Vary e os padrões que uso em produção para sites que aguentam picos sem derreter.
Cache-Control substitui Expires desde 1999 e é obrigatório em qualquer resposta pública. Não dá para deixar ao acaso do browser.
stale-while-revalidate (SWR) permite servir conteúdo "vencido" instantaneamente enquanto revalida em background, cortando TTFB para dezenas de milissegundos.
max-age controla o cache do browser; s-maxage sobrescreve isso apenas para caches compartilhados (CDN, proxy).
Assets com hash no nome (ex.: app.a1b2c3.js) devem ter Cache-Control: public, max-age=31536000, immutable.
Invalidar cache por tag (surrogate keys) é ordens de magnitude mais rápido e barato que purgar por URL individual.
ETag mal configurado atrás de load balancer causa mais problemas do que resolve. Prefira Last-Modified ou hash determinístico.
O que é Cache-Control em HTTP?
Cache-Control é um cabeçalho HTTP de resposta (e opcionalmente de requisição) que define a política de cache para uma resposta específica: quanto tempo pode ser reutilizada, quais intermediários podem armazená-la e como revalidá-la quando "vence". Foi padronizado no HTTP/1.1 (RFC 7234, agora RFC 9111) e é a única forma correta de controlar caching moderno. O antigo Expires ainda existe por compatibilidade, mas depende de relógios sincronizados e não expressa nuances como diretiva por proxy.
Todo hop entre o servidor de origem e o browser do usuário respeita Cache-Control. Isso inclui CDN, proxy corporativo e service worker. Quando o cabeçalho está ausente, cada camada aplica heurísticas próprias (às vezes cacheia por 10% do Last-Modified, às vezes não cacheia nada), e você perde previsibilidade. Em observabilidade real de produção, respostas sem Cache-Control explícito são a primeira causa de comportamento inconsistente entre CDNs. Já vi isso quebrar deploys que "funcionavam local" mas explodiam em Cloudflare quando o cache heurístico servia versão antiga por horas.
A diretiva pode combinar múltiplos valores separados por vírgula: Cache-Control: public, max-age=3600, stale-while-revalidate=86400. A ordem não importa; a interação entre elas sim, e é onde a maioria dos times se atrapalha. Antes de aplicar qualquer estratégia mais elaborada como a redução de TTFB no servidor, garanta que suas respostas têm política de cache explícita para cada content-type. Detalhes técnicos completos estão na documentação MDN sobre Cache-Control.
As diretivas mais usadas em 2026 se dividem em três grupos: freshness (tempo de vida), scope (quem pode cachear) e revalidation (o que fazer quando vence). Entender cada uma isoladamente evita uns 80% dos bugs de cache que aparecem em produção.
max-age e s-maxage
max-age=N define, em segundos, quanto tempo qualquer cache pode considerar a resposta "fresca". s-maxage=N é idêntico, porém aplica-se apenas a caches compartilhados (CDN, proxy reverso). O padrão que uso para HTML dinâmico é:
Isso diz: browsers não cacheiam (max-age=0), mas a CDN cacheia por 60 segundos e pode servir versão stale por até 24 horas enquanto busca uma nova. Resultado: origem recebe no máximo 1 requisição por URL por minuto, TTFB do usuário é o TTFB da borda (uns 20 ms), e mudanças propagam em cerca de 1 minuto sem invalidação manual.
immutable
Para assets com hash no nome (bundles JS, CSS, imagens com fingerprint), immutable promete ao browser que o conteúdo nunca muda. Isso elimina requisições de revalidação mesmo quando o usuário aperta F5:
Um ano é o teto aceito por qualquer cache moderno. Firefox e Safari respeitam immutable integralmente; Chrome trata como max-age padrão, mas o efeito prático é o mesmo porque a URL muda a cada deploy.
public, private, no-store, no-cache
public permite qualquer cache; private restringe ao browser do usuário final (respostas personalizadas). no-store proíbe qualquer armazenamento, então use apenas para dados sensíveis (tokens, PII). E aqui vai o mais mal-interpretado do conjunto: no-cachenão significa "não cacheie". Significa "pode armazenar, mas revalide antes de servir". Se você quer proibir cache, use no-store; se quer forçar revalidação a cada uso, use no-cache.
Como funciona stale-while-revalidate
stale-while-revalidate=N (SWR) é, sinceramente, a diretiva mais importante da última década para performance web. Ela permite que um cache sirva uma resposta expirada imediatamente, enquanto dispara uma revalidação em background para atualizar a entrada. Do ponto de vista do usuário: cache hit sempre. Do ponto de vista do servidor: uma requisição de origem no máximo por janela de revalidação.
Foi padronizada na RFC 5861 em 2010, mas só ganhou adoção real em CDNs a partir de 2020 (Cloudflare, Fastly, Vercel Edge Network) e em browsers com Chrome 75+. Hoje é suportada por todos os navegadores modernos e por praticamente qualquer edge worker.
Anatomia de uma resposta SWR
Cache-Control: max-age=60, stale-while-revalidate=3600
# Timeline (a partir de t=0, primeira request preenche o cache):
# t=0-60s → resposta fresca, serve direto do cache
# t=60-3660s → resposta stale, mas serve do cache E revalida em background
# t=3660s+ → cache expirou totalmente, próxima request bloqueia esperando origem
O comportamento chave: durante a janela stale, o usuário nunca espera. A revalidação acontece após a resposta ser enviada. Se você combinar com stale-if-error=N, o cache ainda serve stale quando a origem retorna erro 5xx. É o padrão ouro para resiliência.
Exemplo prático em Next.js 15 (App Router)
// app/products/[id]/page.tsx
export const revalidate = 60; // ISR padrão
// Ou controle manual do header:
export async function GET(request: Request) {
const data = await fetchProduct();
return new Response(JSON.stringify(data), {
headers: {
'content-type': 'application/json',
'cache-control': 'public, max-age=0, s-maxage=60, stale-while-revalidate=86400',
},
});
}
Essa configuração é o coração do que hoje se chama ISR (Incremental Static Regeneration). Mas você não precisa de nenhum framework para tê-la; um header HTTP correto em qualquer stack faz o mesmo trabalho.
Cache do browser vs cache de borda (CDN)
Existem, no mínimo, três camadas de cache entre sua aplicação e o usuário: o cache HTTP do browser (memory cache mais disk cache), o cache de borda da CDN (POPs distribuídos globalmente) e, opcionalmente, um proxy reverso próximo da origem (Varnish, nginx). Cada uma tem regras próprias, e configurar Cache-Control sem entender essa topologia é jogar dardo no escuro.
Cache do browser
Respeita max-age, ignora s-maxage. Divide-se em memory cache (limpo ao fechar a aba) e disk cache (persiste entre sessões). O disk cache é particionado por site em navegadores modernos desde 2022. Um recurso cacheado em site-a.com não é reutilizado em site-b.com, mesmo com URL idêntica. Isso quebrou muitas estratégias antigas de CDN pública para bibliotecas comuns.
Cache de borda (CDN)
Respeita s-maxage preferencialmente e max-age como fallback. Cada POP mantém seu próprio cache: uma request em São Paulo não popula o cache em Frankfurt. Isso é importante para invalidação: um purge precisa propagar globalmente, o que leva de 50 ms (Cloudflare) a 30 s (CDNs de tier mais baixo).
Service Worker cache
Camada opcional controlada por JavaScript via Cache API. Não respeita Cache-Control automaticamente; o SW decide programaticamente o que fazer. É poderoso para offline-first, mas adiciona uma superfície de bug (cache stale que sobrevive a deploys) e por isso não recomendo para o caso comum de site rápido. Prefira acertar HTTP cache primeiro. Combine com resource hints como preload e preconnect para maximizar hit rate.
ETag, Last-Modified e requisições condicionais
ETag e Last-Modified são cabeçalhos de validação que permitem ao cliente perguntar "o que eu tenho ainda é válido?" antes de baixar o corpo inteiro. Se a resposta é 304 Not Modified, o servidor não envia body, apenas headers, economizando banda e reduzindo TTFB efetivo.
Como o browser usa validadores
# Primeira request:
GET /style.css
< HTTP/1.1 200 OK
< ETag: "a1b2c3"
< Last-Modified: Wed, 15 Jul 2026 10:00:00 GMT
< Cache-Control: max-age=3600
# Após 3600 segundos, browser revalida:
GET /style.css
> If-None-Match: "a1b2c3"
> If-Modified-Since: Wed, 15 Jul 2026 10:00:00 GMT
< HTTP/1.1 304 Not Modified
# (sem body)
ETag forte vs fraco
ETag: "hash" é forte (byte por byte idêntico). ETag: W/"hash" é fraco (semanticamente equivalente). Para HTML dinâmico com pequenas variações (timestamps, IDs de sessão), ETag fraco baseado no hash do conteúdo real (não da resposta completa) evita revalidações desnecessárias.
Estratégias de invalidação: purge, tags e SWR
A frase clássica atribuída a Phil Karlton, "there are only two hard things in computer science: cache invalidation and naming things", continua verdadeira em 2026. Existem três estratégias principais, cada uma com trade-offs específicos.
1. Purge por URL
Você chama um endpoint da CDN passando a URL exata a invalidar. Simples, mas não escala: se um post do blog aparece em /, /blog, /tag/perf e /rss, você precisa purgar quatro URLs a cada edição. Fica pior com paginação e filtros.
2. Purge por tag (surrogate keys)
Você anexa headers de tag a cada resposta (Cache-Tag: post-42,tag-perf,homepage) e depois invalida por tag. Cloudflare Enterprise, Fastly e Vercel implementam isso; para stacks self-hosted, Varnish com xkey resolve. É a estratégia que uso em qualquer CMS: uma tag por entidade, uma por listagem, uma global. Editar um post dispara purge da tag post-42, e todas as páginas que a mencionam se invalidam instantaneamente.
// Middleware que anexa tags na resposta (exemplo em Node)
app.get('/post/:id', async (req, res) => {
const post = await db.getPost(req.params.id);
res.set('Cache-Tag', `post-${post.id},author-${post.authorId},category-${post.categoryId}`);
res.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400');
res.json(post);
});
// Ao editar, invalide por tag via API da CDN:
await fetch('https://api.cloudflare.com/client/v4/zones/<zone>/purge_cache', {
method: 'POST',
headers: { Authorization: `Bearer ${TOKEN}`, 'content-type': 'application/json' },
body: JSON.stringify({ tags: [`post-${postId}`] }),
});
3. SWR sem invalidação
Se o requisito de freshness aceita 30 a 120 segundos de atraso, você pode dispensar invalidação totalmente. TTL curto mais stale-while-revalidate longo faz com que o cache se atualize sozinho, e o custo operacional cai a zero. Uso essa estratégia para conteúdo editorial com deadline flexível. Honestamente, é o padrão que menos quebra em produção.
Vary header: fragmentação e armadilhas
O cabeçalho Vary diz ao cache que a resposta depende do valor de algum request header. Por exemplo, Vary: Accept-Encoding faz o cache manter uma cópia gzip, uma brotli e uma não comprimida. Sem esse header, um cliente que não aceita brotli poderia receber uma resposta comprimida em brotli que não consegue decodificar.
O problema aparece quando você exagera. Vary: User-Agent literalmente cria uma entrada de cache por browser vezes versão vezes OS, e o hit rate despenca para perto de zero. Vary: Cookie é ainda pior: cada cookie de tracking cria uma variação nova. As duas regras que sigo:
Sempre setar Vary: Accept-Encoding se seu servidor negocia compressão (quase todos negociam).
Nunca setar Vary: User-Agent ou Vary: Cookie em rotas cacheáveis. Em vez disso, normalize a chave do cache na CDN (ex.: cookies apenas para bucket A/B/C) ou sirva de rotas diferentes.
Para conteúdo com internacionalização, prefira Vary: Accept-Language apenas se você tem 2 ou 3 idiomas; para 20+ idiomas, use URLs distintas (/pt/, /en/) e evite Vary.
Política de cache por tipo de recurso
Não existe uma política única correta. Cada tipo de recurso tem sua estratégia ótima. Esta é a tabela que uso como default em novos projetos, ajustável conforme o caso.
Para bibliografia de referência sobre estratégia geral, a documentação do web.dev sobre HTTP caching mantém uma versão atualizada dos padrões recomendados pela equipe do Chrome. Vale ler pelo menos uma vez por ano, porque os defaults mudaram várias vezes desde 2020.
Erros comuns de cache HTTP em 2026
Depois de auditar centenas de sites, os mesmos padrões de erro se repetem. Se você identificar qualquer um destes na sua stack, o ganho de corrigir é imediato. Normalmente falamos de 20 a 50% de redução em TTFB percebido e queda drástica de custo de origem.
Servir HTML sem s-maxage
Muitos frameworks (Express default, Rails default) enviam HTML sem Cache-Control, o que faz a CDN aplicar heurística ou passar 100% do tráfego para a origem. Adicionar s-maxage=60, stale-while-revalidate=86400 a rotas GET estáveis pode reduzir requisições à origem em 95%+ da noite para o dia.
Usar no-cache achando que é no-store
Já cobri, mas repito porque é o erro mais comum. no-cache armazena e revalida; no-store proíbe. Se você marcou tokens de sessão com no-cache, eles podem estar em disk cache do browser.
ETag automático em cluster
Se seu HTML é servido de N pods atrás de load balancer e o ETag é auto-gerado, cada pod produz um ETag diferente. Toda revalidação vira miss. Desligue o ETag automático e use ou Last-Modified (consistente entre pods se os arquivos são idênticos) ou nenhum validador (dependendo do max-age puro).
Cache-Control em recursos hashed sem immutable
Se seus bundles JS têm hash no nome mas você não seta immutable, o browser ainda revalida ao apertar F5. Adicionar immutable corta essas requisições de revalidação para zero. É ganho pequeno individualmente, mas em uma SPA com 30 chunks isso soma 30 round-trips economizados.
Purge total ao editar qualquer coisa
Já vi times chamarem "purge all" na CDN a cada deploy ou edição. Isso destrói o cache global, gera stampede de origem e derruba TTFB para todos os usuários por vários minutos. Migre para purge por tag; e mesmo purge por tag deve ser exceção, porque SWR resolve o caso comum sem intervenção. Combine essa disciplina com navegação instantânea via prerendering e bfcache para maximizar cache hits também no client-side.
Ignorar Age nos logs
O header Age (em segundos) diz há quanto tempo a resposta está no cache. Se você não loga isso, não sabe se seu cache está aquecido. Adicione Age e x-cache (hit/miss) aos seus RUM logs. Sem isso, você está otimizando às cegas. Vale ver também as notas da Cloudflare sobre interação de Cache-Control com edge para os detalhes específicos por provedor.
Perguntas frequentes
Qual a diferença entre max-age e s-maxage?
max-age aplica-se a qualquer cache, inclusive o do browser. s-maxage aplica-se apenas a caches compartilhados (CDN, proxy reverso) e, quando presente, sobrescreve max-age para eles. Combine os dois quando quiser políticas diferentes: max-age=0, s-maxage=60 significa "browser não cacheia, CDN cacheia por 60 s".
Devo usar no-cache ou no-store para dados sensíveis?
Use no-store. no-cache permite armazenamento (inclusive em disk cache do browser) e apenas obriga revalidação antes de servir. Para tokens, PII e qualquer resposta que não deve tocar disco, Cache-Control: no-store combinado com Pragma: no-cache (para HTTP/1.0 legado) é o padrão correto.
Como invalido o cache de uma CDN sem purgar tudo?
Use surrogate keys (também chamadas de cache tags). Anexe Cache-Tag: chave1,chave2 às respostas e invalide por tag via API da CDN. Cloudflare Enterprise, Fastly e Vercel suportam nativamente; Varnish self-hosted suporta via módulo xkey. Purge por tag é ordens de magnitude mais barato e rápido que purge por URL.
stale-while-revalidate funciona em todos os browsers?
Sim. Chrome/Edge suportam desde a versão 75 (2019), Firefox desde a 68 e Safari desde a 14.1. Em CDNs, Cloudflare, Fastly, Akamai e Vercel Edge Network implementam totalmente. Para o cache do browser, o suporte é universal em 2026 e não precisa mais de fallback.
Posso cachear HTML dinâmico com dados de usuário?
Não em cache compartilhado. Use Cache-Control: private. Se você precisa da velocidade do edge, extraia o "shell" estático (cacheável) e injete o conteúdo personalizado via ESI (Edge Side Includes), streaming SSR com placeholders ou hidratação client-side. A parte estática vira s-maxage=3600; a parte dinâmica é fetch autenticado.
Guia prático de compressão HTTP em 2026: Brotli, Zstandard e Compression Dictionary Transport, com exemplos de configuração para Nginx, Cloudflare e Fastly, além de scripts de auditoria e comparativos por CDN.
Resource Hints (preload, preconnect, prefetch, dns-prefetch, modulepreload) controlam o caminho crítico da rede e podem cortar centenas de ms do LCP. Guia atualizado para 2026 com exemplos, limites e como medir o ganho real.
TTFB abaixo de 200 ms exige atacar rede, servidor e cache em três frentes. Guia prático com CDN, HTTP/3, Early Hints, streaming SSR e exemplos de código.