MÓDULO 06 · CONCEITO 09 DE 12

HTTP cache RFC 9111

Cache-Control, ETag, Vary, conditional requests. A camada de cache mais ignorada e mais consequente da web — gratuita quando bem usada, vazadora quando mal. Mark Nottingham e a especificação que organizou trinta anos de prática.

Tempo de leitura ~22 min Pré-requisito Conceito 02 (pilha de latência) Próximo CDN e edge caching

Em junho de 2022, a IETF publicou a RFC 9111 — HTTP Caching, atualizando e substituindo a RFC 7234 (de 2014). Editor principal: Mark Nottingham, australiano que escreveu sobre cache HTTP por mais de duas décadas e foi chair do IETF HTTP Working Group. A RFC 9111 não muda dramaticamente o funcionamento de cache; consolida prática conhecida em texto rigoroso, adiciona algumas diretivas modernas (stale-while-revalidate, immutable), e simplifica regras complexas. É a leitura canônica em 2026 sobre o assunto.

HTTP cache é a camada mais antiga e mais robusta de caching que existe na web. Browsers, CDNs, proxies reversos (Varnish, NGINX, Apache), e até o próprio cliente HTTP em código (HttpClient em .NET, requests/ httpx em Python) — todos honram cabeçalhos de cache HTTP automaticamente. Configurar bem Cache-Control e ETag em uma API moderna é gratuito do ponto de vista de infraestrutura, e pode reduzir tráfego ao origin em ordens de magnitude. Configurar mal é fonte canônica de bugs sutis: usuários veem dado obsoleto, ou — incidente clássico — usuário A vê dado de usuário B cacheado por engano.

Apesar de ser camada antiga e madura, surpreendentemente poucos engenheiros sêniores articulam HTTP cache com precisão. Cache-Control: public, max-age=300 é a parte fácil; entender o que Vary faz, quando usar ETag forte vs fraco, qual é a semântica de stale-while-revalidate, e por que private em endpoint autenticado pode vazar — essa é a parte que separa quem usa de quem entende.

Este conceito articula a especificação em prática. Anatomia de cabeçalhos, semântica precisa de cada diretiva, conditional requests com 304 Not Modified, validação por ETag, e os anti-padrões que vazam dado em sistemas autenticados. O conceito 10 (CDN) detalha como essa camada se compõe com a infraestrutura externa.

O modelo conceitual

HTTP cache opera sobre três variáveis principais que o servidor controla via cabeçalhos da resposta.

Cacheabilidade: o servidor diz se a resposta pode ser cacheada (public, private, ou no-store) e por quanto tempo (max-age, s-maxage).

Identidade: o servidor fornece um identificador da versão (ETag) ou timestamp (Last-Modified) que permite ao cache verificar mais tarde se o recurso mudou.

Variabilidade: o servidor declara quais cabeçalhos da request afetam a resposta (Vary) — fundamental para que cache compartilhado não sirva resposta errada para clientes diferentes.

Quando uma request chega a um cache, ele segue sequência: tem entrada para esse URL? Se sim, ela é fresca (dentro do TTL)? Se sim, serve direto (cache hit, latência mínima). Se está expirada, faz conditional request ao origin (envia If-None-Match com ETag, ou If-Modified-Since); o origin responde 304 Not Modified se nada mudou (cache atualiza expiração, serve dado local) ou 200 OK com novo body se mudou (cache substitui).

Esse ciclo torna a maioria das requisições muito mais baratas: quando o recurso não mudou, o origin manda 304 com cabeçalhos vazios — talvez 200 bytes — em vez do JSON de 50 KB. Bandwidth, CPU de serialização, e latência de transmissão, todos reduzidos.

Cache-Control — as diretivas que importam

Cache-Control é o cabeçalho central. As diretivas mais usadas:

public — qualquer cache (browser, CDN, proxy compartilhado) pode armazenar. Apropriado para conteúdo público sem personalização.

private — apenas o cache do usuário final (browser) pode armazenar; CDNs e proxies compartilhados não. Apropriado para conteúdo personalizado por usuário.

no-cache — pode ser armazenado, mas precisa revalidar antes de usar (sempre If-None-Match ao origin). Combinação útil com ETag para entradas que mudam frequentemente mas se beneficiam de validação rápida.

no-store — não armazene em lugar nenhum. Para dado verdadeiramente sensível (token de auth em response body, dados de pagamento).

max-age=N — segundos que a resposta pode ser servida sem revalidar. Aplicada a todos os caches (incluindo browser).

s-maxage=N — sobrescreve max-age para caches compartilhados (CDN, proxy). Permite ter TTL diferente no browser e no CDN.

must-revalidate — cache deve revalidar com origin se o recurso expirou, e não pode servir versão expirada se origin estiver indisponível.

stale-while-revalidate=N — pode servir versão expirada por até N segundos enquanto revalida em background. Ótimo para latência perceptível: o usuário recebe imediatamente conteúdo "ligeiramente velho" enquanto o cache busca atualização.

stale-if-error=N — pode servir versão expirada por N segundos se origin retornar erro. Resiliência baseada em cache.

immutable — o conteúdo nunca mudará na vida do TTL; navegadores podem pular revalidação mesmo se usuário fizer reload. Apropriado para assets versionados (URL com hash do conteúdo).

Receitas comuns por tipo de recurso

A combinação certa varia por tipo de recurso. As receitas canônicas:

Asset estático versionado (/static/app.abc123.js): Cache-Control: public, max-age=31536000, immutable. Um ano de TTL, jamais revalidação. Quando o conteúdo mudar, a URL muda (novo hash), e o cache serve a versão correta automaticamente. Esse é o padrão moderno de bundlers (webpack, vite, esbuild) gerarem URLs com hash.

Asset estático sem versionamento (/favicon.ico): Cache-Control: public, max-age=86400, stale-while-revalidate=604800. Um dia de TTL, mas cache pode servir até uma semana de versão velha enquanto revalida em background. Usuários sempre veem conteúdo recente, sem latência de revalidação.

API JSON pública dinâmica (/api/produtos): Cache-Control: public, max-age=60, stale-while-revalidate=300 + ETag: "...". Cache de 1 minuto, com fallback de 5 min durante revalidação, e ETag para conditional requests.

API JSON personalizada (/api/me): Cache-Control: private, max-age=0, must-revalidate + ETag: "...". Apenas o browser do usuário cacheia; sempre revalida; ETag torna revalidação barata.

Resposta sensível (token, sessão): Cache-Control: no-store. Nunca armazene. Combinar com private é redundante mas defensivo.

ETag e validation — conditional requests

ETag é um identificador opaco da versão do recurso. O servidor calcula (hash do body, número de versão, timestamp) e envia em ETag: "abc123". Cliente armazena junto com o response. Em request seguinte, cliente envia If-None-Match: "abc123". Servidor compara; se ainda é a mesma versão, responde 304 Not Modified com body vazio. Se mudou, responde 200 com novo body e novo ETag.

O ganho é dramático para recursos que mudam raramente: payload de 50 KB vira resposta de poucos bytes. Em sistemas com muito tráfego de leitura, ETag pode reduzir bandwidth ao origin em 90%+.

Strong ETag vs Weak ETag. Strong (ETag: "abc123") significa "byte-por-byte idêntico". Weak (ETag: W/"abc123") significa "semanticamente equivalente, talvez bytes diferentes". Use weak quando o servidor faz transformações idempotentes (ex.: timestamp diferente em metadata mas conteúdo igual). Strong é mais restritivo e raramente é necessário ser restritivo demais.

Calcular ETag tem custo. Se você calcula hash criptográfico do body inteiro a cada request, o ganho de cache pode ser comido pela CPU de hash. Padrões mais baratos: número de versão da entidade no banco (updated_at + row_version), ou hash de campos específicos. Para arquivos estáticos, o servidor (NGINX, Apache, IIS) calcula automaticamente baseado em inode + size + mtime.

Vary — quando o cache compartilhado precisa discriminar

Vary é o cabeçalho que mais confunde — e o que mais vaza dado quando configurado errado. A semântica: "esta resposta varia conforme os valores desses cabeçalhos da request". Cache compartilhado deve manter entradas separadas por cada combinação de valores desses cabeçalhos.

Casos típicos:

Vary: Accept-Encoding — cache mantém versão gzipada e versão sem compressão separadas. Universalmente apropriado em endpoints que negociam compressão.

Vary: Accept-Language — cache mantém versões por idioma. Apropriado em sites multilíngues.

Vary: Authorization — cache mantém versões por valor do header Authorization. Cuidado: cada usuário autenticado tem header diferente, então cache compartilhado vai ter milhões de entradas. Em compartilhado raramente ajuda; melhor é private.

Vary: User-Agent — cache mantém versão por user agent. Quase nunca o que você quer — milhares de variantes de UA quebram cache hit rate.

A regra: declare todos os headers que afetam o conteúdo. Esquecer um vaza resposta entre requests diferentes — usuário A vê resposta gerada para usuário B porque o cache compartilhado considerou as duas requests "iguais". Esse é um dos bugs mais perigosos da web, e é comum em sistemas que adicionam personalização sem atualizar Vary.

Implementação por linguagem

Frameworks modernos têm suporte a HTTP cache de formas distintas. Conhecer o idiomático evita reinventar a roda.

C# — ASP.NET Core OutputCache + ResponseCache
// startup
builder.Services.AddOutputCache(options => {
    options.AddBasePolicy(builder => builder
        .Expire(TimeSpan.FromMinutes(1))
        .SetVaryByHeader("Accept-Encoding"));

    options.AddPolicy("ProductPolicy", builder => builder
        .Expire(TimeSpan.FromMinutes(5))
        .SetVaryByQuery("category", "page")
        .Tag("products"));
});

var app = builder.Build();
app.UseOutputCache();

// endpoint com OutputCache (cache no servidor + cabeçalhos para client)
app.MapGet("/api/produtos/{id}",
    async (Guid id, IProdutoRepository repo, HttpContext ctx) => {
        var produto = await repo.ObterAsync(id);
        if (produto is null) return Results.NotFound();

        // ETag forte baseado no UpdatedAt
        var etag = $"\"{produto.UpdatedAt:O}\"";
        ctx.Response.Headers.ETag = etag;

        // verificar If-None-Match
        if (ctx.Request.Headers.IfNoneMatch == etag)
            return Results.StatusCode(304);

        // Cache-Control para clientes
        ctx.Response.Headers.CacheControl = "public, max-age=60, stale-while-revalidate=300";
        return Results.Ok(produto);
    })
    .CacheOutput("ProductPolicy");

// invalidação por tag em endpoint de update
app.MapPut("/api/produtos/{id}", async (Guid id, AtualizarCmd cmd,
    IProdutoRepository repo, IOutputCacheStore cache) => {
    await repo.AtualizarAsync(id, cmd);
    await cache.EvictByTagAsync("products", default);
    return Results.NoContent();
});

ASP.NET Core 7+ tem OutputCache que combina cache server-side (em memória ou Redis) com cabeçalhos para clientes. Tags facilitam invalidação. Para casos simples, atributo [ResponseCache] emite só os cabeçalhos.

Python — FastAPI + headers manuais ou asgi-cache
from fastapi import FastAPI, Response, Request, status
from datetime import datetime, timedelta
import hashlib, json

app = FastAPI()

@app.get("/api/produtos/{id}")
async def obter_produto(id: str, request: Request, response: Response):
    produto = await repo.obter(id)
    if produto is None:
        raise HTTPException(404)

    # ETag forte (hash do conteúdo)
    body = produto.to_dict()
    etag = '"' + hashlib.sha256(
        json.dumps(body, sort_keys=True).encode()
    ).hexdigest()[:16] + '"'

    # If-None-Match → 304
    if request.headers.get("If-None-Match") == etag:
        return Response(status_code=status.HTTP_304_NOT_MODIFIED)

    response.headers["ETag"] = etag
    response.headers["Cache-Control"] = "public, max-age=60, stale-while-revalidate=300"
    response.headers["Vary"] = "Accept-Encoding"
    return body

# alternativa: asgi-cache (server-side via Redis ou memória)
# pip install asgi-cache
# from asgi_cache import CacheMiddleware
# app.add_middleware(CacheMiddleware, ttl=60, namespace="app")

FastAPI não tem cache HTTP integrado; você manipula response.headers manualmente, ou usa middleware como asgi-cache. Verboso mas explícito — você sabe exatamente o que sai em cabeçalhos.

Go — chi middleware + cabeçalhos manuais
// middleware que adiciona cabeçalhos de cache
func CacheControl(maxAge int, swr int) func(http.Handler) http.Handler {
    val := fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d", maxAge, swr)
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Cache-Control", val)
            w.Header().Set("Vary", "Accept-Encoding")
            next.ServeHTTP(w, r)
        })
    }
}

// handler com ETag e 304
func (h *Handlers) ObterProduto(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    produto, err := h.repo.Obter(r.Context(), id)
    if err != nil {
        http.NotFound(w, r); return
    }

    // ETag baseado em version do banco
    etag := fmt.Sprintf(`"%d"`, produto.Version)

    if r.Header.Get("If-None-Match") == etag {
        w.WriteHeader(http.StatusNotModified)
        return
    }

    w.Header().Set("ETag", etag)
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(produto)
}

// rota
r.With(CacheControl(60, 300)).Get("/api/produtos/{id}", h.ObterProduto)

Go é o mais explícito. Sem framework de cache HTTP universal — cada time monta o que precisa. Bibliotecas como github.com/victorspringer/http-cache oferecem cache server-side, mas não são tão ubíquas quanto OutputCache do .NET.

O incidente clássico — vazamento de dado entre usuários

O bug mais perigoso de HTTP cache em sistema autenticado é vazamento. Cenário canônico: desenvolvedor adiciona cache em endpoint /api/me (perfil do usuário) sem pensar. O cache compartilhado (CDN, proxy reverso) armazena a resposta como "cache hit para essa URL", sem considerar que o conteúdo é específico do usuário. O próximo request a /api/me de qualquer usuário recebe a resposta cacheada — que é do usuário anterior. Tokens, dados pessoais, e-mails, tudo vaza.

As três defesas estruturais:

1. private em endpoints autenticados. Sempre que o response tem personalização por usuário, use Cache-Control: private. Apenas o cache do browser do usuário guarda; CDN/proxy não.

2. Vary: Authorization em último caso. Se o conteúdo de fato precisa ser cacheado em compartilhado (raro), declare Vary: Authorization. Cada usuário terá entrada separada — efetivamente desabilita o cache compartilhado para o endpoint, mas evita vazamento.

3. Tests automatizados. Toda equipe séria tem teste que verifica: /api/me com token A retorna dado de A; depois com token B retorna dado de B; nenhuma das duas respostas tem cabeçalhos Cache-Control: public. CI roda esse teste em cada PR.

armadilha em produção

Cache de página inteira em endpoint que mostra nome do usuário no header. A página /dashboard é mostly static, mas tem um "Olá, Camila" no canto. Equipe configura Cache-Control: public, max-age=300 para "melhorar performance". Sob carga, dois usuários diferentes pegam a mesma resposta cacheada — um vê nome do outro. Defesa: separe a parte personalizada (carrega via JS) do conteúdo público; ou use private; ou nunca cacheie HTML que tem qualquer personalização. Esse é o tipo de incidente que vira manchete em sistemas com escala (já aconteceu com Steam, com Reddit, com várias outras plataformas).

Stale-while-revalidate — UX sem latência de revalidação

stale-while-revalidate (introduzida em RFC 5861, expandida em RFC 9111) é uma das diretivas mais úteis em latência percebida. A semântica: "se a resposta cacheada está expirada mas dentro da janela swr, sirva ela imediatamente e busque atualização do origin em background".

O efeito: usuário recebe resposta em latência de cache hit (microssegundos), mesmo quando o TTL tecnicamente expirou. O cache se atualiza assincronamente, então a próxima request — ou requests subsequentes do mesmo usuário — verão a versão fresca.

É o padrão moderno para conteúdo dinâmico onde "ligeiramente velho" é aceitável. Listas de produtos, feeds, dashboards. Não apropriado para dado transacional crítico (saldo, status de pedido em curso).

stale-if-error é a contrapartida para resiliência: serve versão velha quando origin falha. Combinação típica em sistemas que valorizam uptime: max-age=60, stale-while-revalidate=86400, stale-if-error=86400 — TTL de 1 minuto, mas pode servir até 24h em modo "stale" tanto durante revalidação quanto durante outage.

HTTP cache vs cache de aplicação

Vale articular como HTTP cache (este conceito) se relaciona com cache aplicacional (módulo 05 conceito 10). São camadas distintas com responsabilidades distintas.

HTTP cache opera no protocolo — browser, CDN, proxy reverso. Cobre o caminho cliente → servidor. Granularidade: response inteira por URL + Vary. Invalidação: TTL ou purge explícito (CDN). Visível ao desenvolvedor cliente: o JS que faz fetch respeita o cache HTTP.

Cache aplicacional opera dentro do servidor — Redis, in-memory, HybridCache. Cobre o caminho servidor → banco. Granularidade: por chave arbitrária. Invalidação: por evento, por TTL. Invisível ao cliente.

Os dois se compõem. Request chega ao CDN; CDN serve do seu cache (HTTP cache hit); cliente recebe. Se cache miss, request vai ao origin; origin consulta cache aplicacional para obter dado; serializa em JSON; retorna com cabeçalhos de Cache-Control. CDN armazena. Próxima request do mesmo URL — em qualquer cliente — hit no CDN.

A vitória composta é multiplicativa. Cache HTTP em CDN elimina viagem ao origin para 90% dos requests; cache aplicacional no origin elimina viagem ao banco para 90% dos restantes. Resultado: 99% das requests respondem em < 10 ms; 1% chega ao banco. Sistema que era backed por banco fraco agora aguenta tráfego 100× maior.

heurística do sênior

Antes de cachear qualquer endpoint via HTTP, responda três perguntas. "O conteúdo é igual para todos os usuários, ou depende de identidade?". Se depende, nunca public — sempre private ou cache só por usuário com Vary. "Que cabeçalhos da request afetam o conteúdo?". Liste todos em Vary. Esquecer um vaza. "Qual é o máximo de obsolescência aceitável?". Defina max-age do tamanho da tolerância; stale-while-revalidate para suavizar sem prejudicar. Toda configuração de cache HTTP merece passar por essas três perguntas explicitamente.

Por que importa para a sua carreira

HTTP cache é tema onde a maioria dos engenheiros pleno-sêniores tem conhecimento superficial. Quem articula RFC 9111 com precisão se distingue automaticamente. Em entrevistas para vagas backend, "como você cachearia uma API REST?" é convite para mostrar conhecimento de Cache-Control, ETag, Vary; a resposta forte cita receitas por tipo de recurso e menciona stale-while-revalidate. Em revisão de código de novo endpoint, perceber falta de Cache-Control ou risco de vazamento por Vary ausente é serviço ao time. Em pos-mortem de "usuário viu dado de outro usuário", diagnosticar vazamento por cache vai além de "tem bug" — vai para "configuração de Vary incorreta na resposta X". E em discussão de capacity planning, articular HTTP cache como camada que reduz tráfego ao origin é parte fundamental.

Como praticar

  1. Receitas de cache em três tipos de endpoint. Em projeto seu (ou novo) crie três endpoints com configurações de cache distintas: asset estático versionado (immutable, 1 ano), API pública dinâmica (max-age=60, swr=300, ETag), API autenticada (private, ETag). Use ferramenta de teste (curl, Postman, browser DevTools) para verificar: primeiro request retorna 200 com cabeçalhos; segundo retorna 304 quando manda If-None-Match. Esse exercício torna o ciclo concreto.
  2. Diagnóstico de vazamento. Crie um endpoint /api/me que retorna dados do usuário autenticado. Configure Cache-Control: public, max-age=60 de propósito (anti-padrão). Suba um proxy reverso local (Varnish ou NGINX). Faça request com token A, depois com token B. Verifique que o segundo recebe resposta do primeiro — vazamento. Corrija para private; verifique que o vazamento para. Esse exercício consolida o porquê da regra.
  3. Audit de Cache-Control em projeto existente. Pegue um projeto seu e use ferramenta de inspeção HTTP (browser DevTools, httpie, curl -I) para examinar cabeçalhos de cache de pelo menos 10 endpoints. Para cada um, classifique: está configurado? Está apropriado para o conteúdo? Tem Vary correto? Identifique pelo menos um endpoint com configuração ausente ou errada e proponha correção. Esse é trabalho que tipicamente ninguém faz e que tem retorno alto.

Referências para aprofundar

  1. docs RFC 9111 — HTTP Caching (Mark Nottingham, June 2022). datatracker.ietf.org/doc/html/rfc9111 — A spec atual. Substitui RFC 7234. Lê-se em umas duas horas e cobre tudo de relevante.
  2. docs RFC 5861 — HTTP Cache-Control Extensions for Stale Content (Mark Nottingham, May 2010). A spec original que introduziu stale-while-revalidate e stale-if-error. Curta. Foi incorporada na RFC 9111 mas vale ler como motivação.
  3. livro HTTP: The Definitive Guide — David Gourley, Brian Totty (O'Reilly, 2002). Apesar da idade, capítulo sobre cache permanece referência canônica. Cobre semântica em profundidade que livros mais novos abreviam.
  4. livro High Performance Browser Networking — Ilya Grigorik (O'Reilly, 2013). hpbn.co — Capítulo "Optimizing Application Delivery" cobre HTTP cache em conjunto com HTTP/2 e CDN. Gratuito online.
  5. livro Web Performance in Action — Jeremy Wagner (Manning, 2017). Capítulos sobre caching cobrem padrões práticos com receitas. Voltado para engenheiros front-end mas aplicável a backend.
  6. artigo Caching Tutorial — Mark Nottingham (mnot.net). mnot.net/cache_docs — O autor da RFC 9111 escreveu o tutorial canônico, atualizado periodicamente. Texto curto e didático.
  7. artigo Cache me if you can — Cory House (várias palestras 2017+). YouTube. Apresentação prática sobre Cache-Control em aplicações JS modernas. Útil para sentir o lado cliente.
  8. artigo HTTP Caching Best Practices — Google Web Fundamentals. web.dev/articles/http-cache — Documento oficial do Google com receitas para padrões web modernos. Concise e atual.
  9. docs Cloudflare Cache. developers.cloudflare.com/cache — Documentação canônica de como CDN moderna interpreta cabeçalhos. Inclui diretivas customizadas (cdn-cache-control) e quirks.
  10. docs ASP.NET Core OutputCache. learn.microsoft.com/en-us/aspnet/core/performance/caching/output — Documentação atualizada do mecanismo .NET 7+.
  11. docs Varnish HTTP Cache. varnish-cache.org/docs — Para entender cache compartilhado em profundidade, Varnish é a referência. VCL (Varnish Configuration Language) força articular cada decisão de cache.
  12. artigo Practical HTTP Cache — Kornel Lesiński (kornel.ski). Posts didáticos sobre quirks específicos do HTTP cache em produção. Lesiński é mantenedor do imageoptim e pesquisador de web performance.