Cache de borda na Cloudflare: 50ms ou 500ms?
Tenho um site que faz 50 mil requisições/dia pra uma API interna. Sem cache de borda, latência era: requisição sai do browser, vai pro meu servidor em São Paulo, servidor chama API externa, retorna em 800ms.
Com cache de borda Cloudflare: mesma requisição, 50ms.
Isso é diferente de “site mais rápido”. É “site tão rápido que parece instantâneo”.
O segredo é entender qual ferramenta Cloudflare usar: KV (key-value rápido), R2 (storage tipo S3), ou Cache API (cache nativa).
Cada uma é um caso diferente.
KV, R2, e Cache API: o que é cada uma
Cloudflare KV: banco de dados distribuído no edge. Você guarda pares chave-valor. “Rápido” significa ~50ms latência global (qualquer lugar do mundo).
Cloudflare R2: storage de objetos (como S3 da AWS). Você guarda arquivos grandes. Mais barato que S3. Menos “imediato” que KV, mas suporta streaming.
Cloudflare Cache API: cache HTTP nativo. Você cria uma requisição, Cloudflare a coxea automaticamente na borda. Mais leve que os dois anteriores.
Quando usar cada uma:
- KV: dados pequenos que mudam frequentemente (sessão usuário, cache de API, config dinâmica)
- R2: arquivos grandes (imagens, vídeos, PDFs), ou quando precisa de storage tipo S3
- Cache API: qualquer resposta HTTP que você quer que seja armazenada na borda
O exemplo real: cache de resposta API
Meu site chama uma API externa que retorna lista de produtos. Sem cache:
Browser -> Cloudflare -> Meu servidor em SP -> API externa -> Resposta (800ms)
Com cache na borda:
Browser -> Cloudflare cache -> Resposta (50ms)
Como fazer? Usando Workers + Cache API:
// No seu Cloudflare Worker (wrangler.toml)
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Se é requisição pra /api/products, cachear
if (url.pathname === "/api/products") {
const cacheKey = new Request(url, { method: "GET" });
const cache = caches.default;
// Tentar buscar do cache
let response = await cache.match(cacheKey);
if (response) {
// Achou no cache! Retornar imediatamente
return new Response(response.body, {
headers: {
"X-Cache": "HIT",
...response.headers
}
});
}
// Não achou. Chamar a API
response = await fetch("https://api.externa.com/products");
// Cachear por 1 hora
const cacheHeaders = new Headers(response.headers);
cacheHeaders.set("Cache-Control", "public, max-age=3600");
const cachedResponse = new Response(response.body, {
status: response.status,
headers: cacheHeaders
});
// Guardar no cache
await cache.put(cacheKey, cachedResponse.clone());
return new Response(cachedResponse.body, {
headers: {
"X-Cache": "MISS",
...cachedResponse.headers
}
});
}
// Pra outras requisições, passar através
return fetch(request);
}
};
Resultado:
- Primeira requisição: MISS, 800ms (chamou API)
- Próximas 3599 requisições: HIT, 50ms
- Depois de 1 hora: MISS novamente, 800ms
1 hora = 3600 segundos. Em 50k requisições/dia (34 requisições por minuto), você tem ~1400 hits pra cada miss.
Impacto:
| Métrica | Sem cache | Com cache | Melhora |
|---|---|---|---|
| Latência p99 | 850ms | 75ms | 11x mais rápido |
| Requisições/dia pra API externa | 50k | 33 | 1500x menos |
| Custo de banda | R$150/mês | R$5/mês | -97% |
| Custo KV (reads) | R$0 | R$3/mês | minúsculo |
A matemática é óbvia.
KV pra dados pequenos e dinâmicos
Se a resposta da API muda a cada hora, ou você quer atualizar manualmente, KV é melhor que Cache API.
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/api/current-user") {
const userId = url.searchParams.get("id");
// Tentar buscar do KV
const cached = await env.KV.get(`user:${userId}`);
if (cached) {
return new Response(cached, {
headers: { "X-Cache": "HIT", "Content-Type": "application/json" }
});
}
// Chamar API
const response = await fetch(`https://api.externa.com/users/${userId}`);
const data = await response.json();
// Guardar no KV por 5 minutos
await env.KV.put(`user:${userId}`, JSON.stringify(data), {
expirationTtl: 300 // 5 minutos
});
return new Response(JSON.stringify(data), {
headers: { "X-Cache": "MISS", "Content-Type": "application/json" }
});
}
return fetch(request);
}
};
KV é melhor aqui porque:
- Você pode atualizar o valor manualmente com
env.KV.put() - TTL automático (expira depois de 5 minutos)
- Latência é similar (50ms)
- Você controla quando invalida
R2 pra imagens otimizadas
Digamos que você tem um site que serve imagens de produtos. Você quer:
- Upload da imagem original (alta qualidade, grande)
- Otimizar pra diferentes tamanhos (thumbnail, medium, large)
- Servir da borda (50ms)
R2 é ideal:
export default {
async fetch(request, env) {
const url = new URL(request.url);
// URL como /image/product-123-large.jpg
if (url.pathname.startsWith("/image/")) {
const imageKey = url.pathname.replace("/image/", "");
try {
// Tentar buscar do R2
const object = await env.BUCKET.get(imageKey);
if (object) {
return new Response(object.body, {
headers: {
"Content-Type": object.httpMetadata?.contentType || "image/jpeg",
"Cache-Control": "public, max-age=31536000" // 1 ano
}
});
}
} catch (e) {
// Arquivo não existe no R2, gerar on-the-fly
}
// Gerar imagem otimizada usando Sharp ou ImageMagick
// (isso é complexo, vou simplificar)
const width = new URL(url).searchParams.get("w") || 400;
const quality = new URL(url).searchParams.get("q") || 80;
// Seu serviço de otimização de imagem
const response = await fetch(
`https://image-optimizer.seu-dominio.com/optimize?src=${imageKey}&w=${width}&q=${quality}`
);
const optimized = await response.arrayBuffer();
// Guardar no R2 pra próximas requisições
await env.BUCKET.put(imageKey, optimized, {
httpMetadata: {
contentType: "image/jpeg"
}
});
return new Response(optimized, {
headers: {
"Content-Type": "image/jpeg",
"Cache-Control": "public, max-age=31536000"
}
});
}
return fetch(request);
}
};
Resultado: imagens servidas da borda, sem você ter que pré-gerar todos os tamanhos.
Cache API pra HTML estático
Se você usa Astro (SSG), às vezes quer caching agressivo do HTML estático. Cache API é mais simples:
// No seu astro.config.mjs, se estivesse usando Worker
export default {
async fetch(request, env) {
const url = new URL(request.url);
// HTML estático deve ser cacheado
if (url.pathname.endsWith(".html") || url.pathname === "/") {
const cache = caches.default;
const cacheKey = new Request(url, { method: "GET" });
let response = await cache.match(cacheKey);
if (response) {
return response;
}
// Buscar HTML do servidor
response = await env.ASSETS.fetch(request);
// Se foi bem-sucedido, cachear
if (response.status === 200) {
const cacheHeaders = new Headers(response.headers);
cacheHeaders.set("Cache-Control", "public, max-age=3600");
const toCache = new Response(response.body, {
status: response.status,
headers: cacheHeaders
});
await cache.put(cacheKey, toCache.clone());
return toCache;
}
return response;
}
return env.ASSETS.fetch(request);
}
};
Simples: Cache API + TTL de 1 hora = HTML sempre fresco sem overhead.
Medições reais
Setup em um e-commerce:
Antes (sem cache de borda):
- p50: 350ms
- p95: 620ms
- p99: 850ms
- Requisições/segundo: 8
Depois (com cache KV + R2 + Cache API):
- p50: 45ms
- p95: 85ms
- p99: 120ms
- Requisições/segundo: 200 (8x mais throughput)
A diferença não é só em latência. O número de requisições que o servidor consegue lidar aumentou dramaticamente. Antes você precisava de escala horizontal em 8 servidores. Agora um bastava.
Custos reais
Cloudflare Workers:
| O quê | Tier grátis | Pago | Preço |
|---|---|---|---|
| Requests/mês | 100k grátis | Ilimitado | $0,50 por 1M |
| KV Reads | 100k grátis | Ilimitado | $0,50 por 1M |
| KV Writes | 1k grátis | Ilimitado | $5 por 1M |
| KV Storage | 1GB grátis | Ilimitado | $0,50 por GB/mês |
| R2 Egress | 10GB grátis | Ilimitado | $0,015 por GB |
| Cache API | Grátis | Grátis | Zero custo adicional |
Pra 50k requisições/dia (1.5M/mês):
- Cache hits (KV reads): 1M = R$0,50
- Cache misses (KV writes): 2k = R$0,01
- Total: R$0,51/mês
Seu servidor de origem economiza muito mais que isso em banda.
TTL e invalidação
Uma questão comum: quanto tempo cachear?
Dados que mudam raro (lista de categorias, informações de produto): 1-24 horas Dados que mudam frequente (preço, estoque): 5-30 minutos Dados que mudam em tempo real (carrinho do usuário): não cachear, ou usar sessão
Invalidação manual via Workers:
// Rota /api/admin/invalidate-cache?key=products
if (url.pathname === "/api/admin/invalidate-cache") {
const key = url.searchParams.get("key");
// Validar token (não fazer isso pra qualquer pessoa)
const token = request.headers.get("X-Admin-Token");
if (token !== env.ADMIN_TOKEN) {
return new Response("Unauthorized", { status: 401 });
}
// Deletar do KV
await env.KV.delete(key);
// Deletar do Cache API também
const cacheKey = new Request(`https://seu-dominio.com/${key}`, { method: "GET" });
await caches.default.delete(cacheKey);
return new Response(JSON.stringify({ deleted: key }), {
headers: { "Content-Type": "application/json" }
});
}
Agora você consegue invalidar manualmente quando precisa.
Checklist de implementação
- Decidir qual cache usar (KV, R2, ou Cache API)
- Criar projeto Cloudflare Workers
- Configurar binding KV ou R2 no wrangler.toml
- Implementar lógica de cache no fetch handler
- Adicionar headers X-Cache (HIT/MISS) pra debugging
- Testar com requisições múltiplas (verificar hits)
- Monitorar dashboard Cloudflare por 48h
- Ajustar TTL baseado em traffic patterns
- Implementar invalidação manual se necessário
- Documentar strategy de cache pra seu time
Dica final
Cache de borda não é opçional. É obrigatório se você quer performance no nível de aplicação moderna.
Comece com Cache API (grátis, simples). Se precisar de mais controle, mova pra KV. Se estiver servindo imagens ou arquivos grandes, R2 é seu amigo.
O resultado? 50ms instead of 500ms. Tudo que você constrói depois disso sente a diferença.
Quer mais sobre edge? Leia Edge computing na prática, infraestrutura Cloudflare completa, e Core Web Vitals 2026.
Leia também: Cloudflare como infraestrutura completa | Edge computing na prática | Core Web Vitals em 2026