CDN
Servidores perto do usuário para entregar arquivos e respostas sem chegar à sua aplicação.
O código desta página foi executado nas cinco linguagens, cada versão atrás de um Nginx 1.31 configurado como borda de cache. As medições do nível Conceito usaram a mesma borda.
A origem com os cabeçalhos certos, em cada linguagem
Cada versão tem duas rotas. /produtos/7 é pública: leva um Cache-Control que deixa a borda guardar por 5 minutos e o navegador por 1, aceita a cópia vencida durante a revalidação e se a origem cair, e tem um ETag. Quando o pedido traz If-None-Match com o mesmo ETag, a resposta é 304, sem corpo. /minha-conta devolve dados do usuário e proíbe qualquer cache compartilhado.
import crypto from "node:crypto";
import http from "node:http";
// Dado público: a CDN guarda por 5 minutos, o navegador por 1, e ambos aceitam
// a cópia vencida enquanto revalidam ou se a origem cair
const CACHE_PUBLICO =
"public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=86400";
http.createServer((req, res) => {
if (req.url === "/produtos/7") {
const corpo = JSON.stringify({ id: 7, nome: "Tênis de corrida", preco: "19.90" });
const etag = `"${crypto.createHash("sha256").update(corpo).digest("hex").slice(0, 16)}"`;
res.setHeader("Cache-Control", CACHE_PUBLICO);
res.setHeader("ETag", etag);
if (req.headers["if-none-match"] === etag) {
res.statusCode = 304; // a cópia de quem pediu continua valendo: sem corpo
return res.end();
}
res.setHeader("Content-Type", "application/json");
return res.end(corpo);
}
if (req.url === "/minha-conta") {
// Dado de um usuário: nenhum cache compartilhado pode guardar
res.setHeader("Cache-Control", "private, no-store");
res.setHeader("Content-Type", "application/json");
return res.end(JSON.stringify({ usuario: req.headers["x-usuario"] }));
}
res.statusCode = 404;
res.end();
}).listen(3000);import hashlib
import json
from fastapi import FastAPI, Header, Response
# Dado público: a CDN guarda por 5 minutos, o navegador por 1, e ambos aceitam
# a cópia vencida enquanto revalidam ou se a origem cair
CACHE_PUBLICO = "public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=86400"
app = FastAPI()
@app.get("/produtos/7")
def produto(if_none_match: str | None = Header(None)):
corpo = json.dumps({"id": 7, "nome": "Tênis de corrida", "preco": "19.90"},
ensure_ascii=False, separators=(",", ":"))
etag = '"' + hashlib.sha256(corpo.encode()).hexdigest()[:16] + '"'
cabecalhos = {"Cache-Control": CACHE_PUBLICO, "ETag": etag}
if if_none_match == etag:
return Response(status_code=304, headers=cabecalhos) # a cópia continua valendo
return Response(corpo, media_type="application/json", headers=cabecalhos)
@app.get("/minha-conta")
def minha_conta(x_usuario: str | None = Header(None)):
# Dado de um usuário: nenhum cache compartilhado pode guardar
return Response(json.dumps({"usuario": x_usuario}), media_type="application/json",
headers={"Cache-Control": "private, no-store"})package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
)
// Dado público: a CDN guarda por 5 minutos, o navegador por 1, e ambos aceitam
// a cópia vencida enquanto revalidam ou se a origem cair
const cachePublico = "public, max-age=60, s-maxage=300, " +
"stale-while-revalidate=60, stale-if-error=86400"
func produto(w http.ResponseWriter, r *http.Request) {
corpo, _ := json.Marshal(map[string]any{"id": 7, "nome": "Tênis de corrida", "preco": "19.90"})
soma := sha256.Sum256(corpo)
etag := `"` + hex.EncodeToString(soma[:])[:16] + `"`
w.Header().Set("Cache-Control", cachePublico)
w.Header().Set("ETag", etag)
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified) // a cópia de quem pediu continua valendo
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(corpo)
}
func minhaConta(w http.ResponseWriter, r *http.Request) {
// Dado de um usuário: nenhum cache compartilhado pode guardar
w.Header().Set("Cache-Control", "private, no-store")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"usuario": r.Header.Get("X-Usuario")})
}
func main() {
http.HandleFunc("GET /produtos/7", produto)
http.HandleFunc("GET /minha-conta", minhaConta)
http.ListenAndServe(":3000", nil)
}import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
public class Origem {
// Dado público: a CDN guarda por 5 minutos, o navegador por 1, e ambos aceitam
// a cópia vencida enquanto revalidam ou se a origem cair
static final String CACHE_PUBLICO =
"public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=86400";
static void produto(HttpExchange ex) throws IOException {
byte[] corpo = "{\"id\":7,\"nome\":\"Tênis de corrida\",\"preco\":\"19.90\"}"
.getBytes(StandardCharsets.UTF_8);
String etag;
try {
var soma = MessageDigest.getInstance("SHA-256").digest(corpo);
etag = "\"" + HexFormat.of().formatHex(soma).substring(0, 16) + "\"";
} catch (Exception e) {
throw new IOException(e);
}
ex.getResponseHeaders().set("Cache-Control", CACHE_PUBLICO);
ex.getResponseHeaders().set("ETag", etag);
if (etag.equals(ex.getRequestHeaders().getFirst("If-None-Match"))) {
ex.sendResponseHeaders(304, -1); // a cópia de quem pediu continua valendo
return;
}
ex.getResponseHeaders().set("Content-Type", "application/json");
ex.sendResponseHeaders(200, corpo.length);
ex.getResponseBody().write(corpo);
}
static void minhaConta(HttpExchange ex) throws IOException {
// Dado de um usuário: nenhum cache compartilhado pode guardar
byte[] corpo = ("{\"usuario\":\"" + ex.getRequestHeaders().getFirst("X-Usuario") + "\"}")
.getBytes(StandardCharsets.UTF_8);
ex.getResponseHeaders().set("Cache-Control", "private, no-store");
ex.getResponseHeaders().set("Content-Type", "application/json");
ex.sendResponseHeaders(200, corpo.length);
ex.getResponseBody().write(corpo);
}
public static void main(String[] args) throws IOException {
var servidor = HttpServer.create(new InetSocketAddress(3000), 0);
servidor.createContext("/produtos/7", ex -> { try (ex) { produto(ex); } });
servidor.createContext("/minha-conta", ex -> { try (ex) { minhaConta(ex); } });
servidor.start();
}
}using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
// Dado público: a CDN guarda por 5 minutos, o navegador por 1, e ambos aceitam
// a cópia vencida enquanto revalidam ou se a origem cair
const string CachePublico =
"public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=86400";
var app = WebApplication.Create(args);
app.MapGet("/produtos/7", (HttpContext ctx) =>
{
// Sem escapar acentos, para os mesmos bytes (e o mesmo ETag) das outras versões
var opcoes = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
var corpo = JsonSerializer.Serialize(new { id = 7, nome = "Tênis de corrida", preco = "19.90" }, opcoes);
var hash = Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(corpo)));
var etag = $"\"{hash[..16]}\"";
ctx.Response.Headers.CacheControl = CachePublico;
ctx.Response.Headers.ETag = etag;
if (ctx.Request.Headers.IfNoneMatch == etag)
return Results.StatusCode(304); // a cópia de quem pediu continua valendo
return Results.Text(corpo, "application/json");
});
app.MapGet("/minha-conta", (HttpContext ctx) =>
{
// Dado de um usuário: nenhum cache compartilhado pode guardar
ctx.Response.Headers.CacheControl = "private, no-store";
return Results.Json(new { usuario = ctx.Request.Headers["X-Usuario"].ToString() });
});
app.Run("http://0.0.0.0:3000");O resultado, nas cinco linguagens
| Pedido | Esperado | Nas cinco |
|---|---|---|
/produtos/7, primeira vez, pela borda | MISS | MISS |
/produtos/7, segunda vez, pela borda | HIT | HIT |
/produtos/7 na origem com If-None-Match | 304 | 304 |
/minha-conta da Ana e depois do Bruno, pela borda | Cada um vê a sua | ana e bruno |
As cinco versões produziram exatamente o mesmo corpo e o mesmo ETag ("e5f79621d52f5e5a"). Isso tem efeito prático: se várias instâncias, ou serviços em linguagens diferentes, atendem a mesma rota, o ETag precisa sair igual em todas, ou a revalidação falha e a resposta inteira volta a cada vez.
O que muda de uma linguagem para outra
ETag a partir do corpo | Mesmo JSON, byte a byte | |
|---|---|---|
| TypeScript | crypto.createHash("sha256") | JSON.stringify já produz o formato compacto |
| Python | hashlib.sha256 | json.dumps com separators=(",", ":") e ensure_ascii=False; o padrão põe espaços e escapa o "ê" |
| Go | sha256.Sum256 | json.Marshal ordena as chaves de um mapa em ordem alfabética |
| Java | MessageDigest | O JSON montado à mão, só para o exemplo |
| C# | SHA256.HashData | JavaScriptEncoder.UnsafeRelaxedJsonEscaping; o padrão escapa o "ê" como ê |
O "mesmo JSON" exigiu cuidado em três linguagens: sem os ajustes, o Python e o C# escapariam o acento, e o ETag sairia diferente para o mesmo dado. Em vez de calcular o ETag do corpo, muitos sistemas usam a versão do registro (updated_at ou um contador), que é igual em qualquer linguagem e evita montar o corpo para responder 304.
Alguns frameworks têm atalhos: o Express gera um ETag por padrão e oferece req.fresh para responder 304, e o Spring tem o ShallowEtagHeaderFilter. O cuidado continua o mesmo: o Cache-Control certo em cada rota.
A borda do teste
A configuração do Nginx que fez o papel da CDN:
proxy_cache_path /tmp/cache keys_zone=borda:10m max_size=100m inactive=1h;
server {
listen 80;
location / {
proxy_pass http://origem:3000;
proxy_cache borda;
proxy_cache_key "$request_uri";
proxy_cache_use_stale error timeout http_502 http_503; # cópia vencida se a origem cair
add_header X-Cache $upstream_cache_status always; # HIT, MISS, STALE
}
}O cabeçalho X-Cache mostra o que aconteceu em cada pedido; as CDNs comerciais têm equivalentes (CF-Cache-Status na Cloudflare, X-Cache no CloudFront). Ele é a primeira coisa a olhar quando o cache não acerta.
Lista de revisão
Cabeçalhos
- Toda resposta tem um
Cache-Controlexplícito, definido por rota? - Rotas com dados do usuário têm
private, no-store, com teste automatizado? - Páginas e APIs públicas têm
s-maxage,stale-while-revalidateestale-if-error? - Arquivos estáticos têm nome versionado, cache longo e
immutable? - As respostas públicas têm
ETag, igual em todas as instâncias?
Acertos de cache
- Páginas públicas saem sem
Set-Cookie? - Parâmetros de rastreamento estão fora da chave do cache?
- O
Varytem só o necessário? - A taxa de acerto da CDN é acompanhada, e o
X-Cache(ou equivalente) é conferido quando cai?
Invalidação
- Há um caminho para limpar o cache quando o conteúdo muda, por caminho ou por etiqueta?
- O prazo do cache é aceitável se a limpeza falhar?
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Definir os cabeçalhos de cache na aplicação
Quero que a minha aplicação envie os cabeçalhos de cache certos em cada rota. Pergunte qual linguagem e framework eu uso e quais rotas existem. Vamos em etapas: 1. Uma forma central de declarar o cache de cada rota. 2. Cache-Control para estáticos, páginas públicas, APIs públicas e rotas autenticadas. 3. ETag e respostas 304. 4. Remover sessões e cookies das respostas públicas. 5. Um teste que confere o Cache-Control de cada rota, em especial as autenticadas.
Montar uma borda local para testar o cache
Quero testar o comportamento de cache da minha aplicação localmente, antes de colocar uma CDN. Pergunte qual linguagem eu uso e como a aplicação sobe. Me ajude a montar: 1. Um Nginx com proxy_cache na frente da aplicação, mostrando HIT, MISS e STALE num cabeçalho. 2. Um atraso de rede simulado para a origem. 3. Testes que confirmam o cache das rotas públicas e a ausência de cache nas autenticadas. 4. Um teste com a origem desligada. 5. A comparação com o que a CDN de produção vai fazer.
Revisar a configuração de cache e CDN
Vou colar abaixo os cabeçalhos de cache das rotas da minha aplicação e a configuração da CDN. Revise. Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho: 1. Rotas com dados do usuário estão protegidas com private ou no-store? 2. Páginas e APIs públicas podem ser guardadas pela CDN, com stale-while-revalidate e stale-if-error? 3. Há Set-Cookie em respostas públicas? 4. A chave do cache ignora parâmetros de rastreamento? 5. Estáticos têm nome versionado e cache longo? 6. Há um caminho de invalidação? Para cada problema, descreva o efeito em produção e mostre a correção. Cabeçalhos e configuração: [cole aqui]