Cloudflare Workers KV na prática: do básico ao avançado em armazenamento chave-valor distribuído

Eu estava olhando para o gráfico de latência no Cloudflare Dashboard. A linha vermelha continuava acima dos 200 ms: mesmo usando Workers e com um código já bastante enxuto, por que cada solicitação ainda demorava tanto?
O problema estava no banco de dados. Cada consulta de sessão precisava sair do nó de borda, chegar ao data center na Europa e voltar. Mesmo que a execução do Worker levasse apenas 5 ms, a transmissão pela rede consumia todo o tempo restante.
Foi então que entendi: Workers são stateless. Eu precisava de um armazenamento que realmente “morasse na borda” — o Cloudflare Workers KV.
Neste artigo, reúno os problemas que encontrei, os dados que medi e o código que escrevi. Vamos entender o que é KV, como ele alcança latência abaixo de 10 ms e como implementar por completo armazenamento de sessões e cache de API. Também veremos quando usar KV e quando D1 ou R2 são opções melhores.
O que é KV — entendendo o armazenamento distribuído na borda
Em termos simples, KV é como uma “memória portátil” que a Cloudflare disponibiliza aos Workers. Ela não fica em um único data center: é distribuída por mais de 300 nós de borda ao redor do mundo. Para um usuário em Tóquio, os dados podem estar esperando no nó de Tóquio; para uma solicitação em Frankfurt, eles talvez já estejam no cache de Frankfurt.
Cloudflare Workers KV é um armazenamento chave-valor distribuído globalmente, criado para edge computing. Ele tem três características principais:
Leitura muito rápida. Em cache, hot keys apresentam latência de 500 µs a 10 ms. Confesso que desconfiei desses números na primeira vez em que os vi, mas, depois de executar meus próprios benchmarks, consegui manter resultados estáveis na casa de poucos milissegundos.
Replicação global. Ao gravar um dado, ele é replicado para nós de borda no mundo inteiro. Isso é um pouco diferente de um cluster Redis: o modelo do KV é “gravar uma vez, ler em qualquer lugar”, ideal para cenários com muitas leituras e poucas gravações.
Alta vazão. A leitura de uma única chave pode alcançar milhares de RPS (requests per second), pois os dados ficam no cache de borda e não precisam acessar a origem a cada solicitação.
Comparativo do ecossistema de armazenamento da Cloudflare
KV é apenas uma peça da matriz de armazenamento da Cloudflare. Veja o panorama completo:
| Serviço de armazenamento | Modelo de dados | Melhor uso | Limite de gravação | Característica de latência |
|---|---|---|---|---|
| KV | Chave-valor | Sessão, cache, configuração | 1 RPS/chave | hot keys: 500 µs-10 ms |
| D1 | SQL (SQLite) | Dados de usuários, pedidos, relatórios | Sem limite rígido | Depende da localização, normalmente 50-200 ms |
| R2 | Object Storage | Arquivos, imagens, vídeos | Sem limite rígido | Download rápido; upload depende do tamanho do arquivo |
| Durable Objects | Objetos com estado | Edição colaborativa, WebSocket | Sem limite rígido | Precisa localizar um nó específico |
Talvez você esteja se perguntando o que significa 1 RPS/chave. Veremos isso em detalhes mais adiante; por enquanto, basta saber que cada chave só pode ser gravada uma vez por segundo, a principal limitação que exige atenção no KV.
Consulta rápida de casos de uso do KV
Quando vale a pena considerar KV? Use este critério simples:
KV é recomendado para:
- Session storage (estado de login do usuário)
- Cache de respostas de API (retorno de APIs de terceiros)
- Contadores de rate limiting (limitação de frequência)
- Feature flags e dados de configuração
- Redirect mapping (regras de redirecionamento de URL)
KV não é recomendado para:
- Dados gravados com frequência, como contadores em tempo real acima de 1 RPS
- Dados complexos que exigem SQL, como tabelas de usuários e pedidos; use D1
- Arquivos grandes, como imagens e vídeos; use R2
- Transações financeiras que exigem consistência forte; use Durable Objects
A documentação oficial da Cloudflare é clara: KV é indicado para cenários com “alta taxa de leitura, baixa frequência de alteração e sem necessidade de consistência imediata”. Frameworks de autenticação como OpenAuth também usam KV como session storage padrão. Mais adiante, mostrarei uma implementação completa.
Arquitetura do KV em detalhes — por que ele é tão rápido
A velocidade do KV não é mágica, mas o resultado de uma arquitetura de cache em três camadas.
Imagine que você está comprando algo em uma loja de conveniência. No melhor caso, o produto está na prateleira ao lado do caixa e basta estender a mão (edge cache). Em um caso menos favorável, ele está no estoque e o atendente precisa buscá-lo (regional cache). No caso mais lento, está no centro de distribuição e você precisa esperar o caminhão chegar (central store).
A arquitetura do KV segue essas três camadas:
Solicitação → Edge Cache (mais rápido)
↓ cache miss
Regional Cache
↓ cache miss
Central Store (mais lento)
Segundo dados publicados pela Cloudflare em outubro de 2025, cerca de 30% das solicitações são atendidas diretamente pelas camadas de cache. Isso significa que um terço das leituras nem precisa retornar ao armazenamento central, reduzindo naturalmente a latência.
Dados de desempenho: dos números oficiais aos testes práticos
A documentação oficial da Cloudflare fornece estes valores de referência:
- Hot keys (chaves acessadas com frequência): 500 µs a 10 ms
- Cold keys (primeiro acesso ou acesso raro): latência maior, pois precisam chegar à origem
Para ser sincero, inicialmente não acreditei nos “500 µs”. Depois, fiz meu próprio teste:
// Código simples para testar a latência
const start = Date.now();
await env.KV.get("test-key");
const latency = Date.now() - start;
console.log(`Latency: ${latency}ms`);
Em 100 execuções, a latência média das hot keys ficou de fato em torno de 5 a 8 ms. No primeiro acesso, cold keys passaram de 50 ms, mas o segundo acesso já foi mais rápido: o cache havia entrado em ação.
Em 2025, a Cloudflare fez uma grande reformulação do KV. Segundo o blog oficial, as operações ficaram três vezes mais rápidas. As principais mudanças foram:
- Conexão direta entre Workers e KV, sem passar pela antiga camada Front Line
- Simplificação do caminho interno de transferência de dados
A mudança também trouxe ganhos em cascata para outros serviços da Cloudflare que dependem de KV, como Turnstile e Waiting Room.
Modelo de consistência: o custo da consistência eventual
KV usa consistência eventual (eventually consistent). O que isso quer dizer?
Ao gravar um dado, ele não aparece imediatamente em todos os nós de borda. A propagação leva algum tempo. A Cloudflare não informa um número exato, mas, nos testes práticos, a propagação entre regiões costuma levar de alguns segundos a dezenas de segundos.
Em alguns cenários, isso é um problema; em outros, não faz diferença:
Cenários problemáticos:
- O usuário acaba de fazer login e a sessão foi gravada no KV, mas a próxima solicitação chega a outro nó, onde a sessão ainda não existe; o login parece ter “falhado”
- Em uma edição colaborativa em tempo real, o usuário A altera algo e o usuário B faz uma leitura imediata, mas ainda não vê o conteúdo mais recente
Cenários sem problema:
- Em configurações de feature flags, esperar alguns segundos para uma alteração entrar em vigor é perfeitamente aceitável
- Em cache de API, o retorno de uma API de terceiros fica armazenado por alguns minutos, portanto a propagação atrasada não importa
- Em redirect mapping, regras de URL podem demorar alguns segundos para atualizar sem que quase nenhum usuário perceba
Se o seu cenário exige consistência imediata, KV talvez não seja adequado. Nesse caso, Durable Objects pode ser uma opção melhor, pois localiza o estado em um nó específico e preserva a consistência.
Configuração prática com Wrangler CLI
Depois da teoria, vamos à prática.
A configuração do KV tem duas partes: criar um namespace e vinculá-lo ao Worker em wrangler.toml.
Criar um namespace
Namespace é o “contêiner” do KV. Cada namespace pode guardar inúmeros pares chave-valor, mas uma conta pode ter no máximo 1.000 namespaces; no início de 2025, esse limite subiu de 200 para 1.000.
# Criar o namespace de produção
wrangler kv namespace create MY_KV
# A saída será semelhante a esta:
# Created namespace with id "abc123def456..."
# Add the following to your wrangler.toml:
# [[kv_namespaces]]
# binding = "MY_KV"
# id = "abc123def456..."
Você também precisa de um namespace de preview para testes no desenvolvimento local:
# Criar o namespace de preview
wrangler kv namespace create MY_KV --preview
# Saída semelhante a:
# Created preview namespace with id "preview_abc123..."
Configuração detalhada do wrangler.toml
Insira em wrangler.toml os IDs gerados acima:
name = "my-worker"
main = "src/index.ts"
[[kv_namespaces]]
binding = "MY_KV"
id = "abc123def456..." # namespace de produção
preview_id = "preview_abc123..." # namespace de preview (desenvolvimento local)
O nome de binding é importante. Ele determina como você acessará o KV no código do Worker:
// Com binding = "MY_KV", use env.MY_KV no código
const value = await env.MY_KV.get("some-key");
REST API vs Workers Binding API
Há duas formas de acessar os dados do KV:
Workers Binding API (recomendada):
- Use
env.MY_KV.get()diretamente no Worker - Não faz uma solicitação de rede adicional e oferece o melhor desempenho
- Não tem custo adicional; só conta no tempo de execução do Worker
REST API:
- Acessa o KV por meio de solicitações HTTP
- Exige token de autenticação e é indicada para sistemas externos
- Está sujeita ao limite geral de requisições da REST API da Cloudflare
Na prática, quase todos os cenários deveriam usar a Binding API. A REST API é usada principalmente quando:
- Um sistema externo precisa ler ou gravar dados no KV
- Um fluxo de CI/CD precisa importar dados em lote
- É necessário fazer depuração ou manutenção pontual
Comandos comuns do Wrangler KV
O Wrangler oferece ferramentas de linha de comando para trabalhar com dados do KV:
# Gravar dados
wrangler kv key put --namespace-id=abc123 "my-key" "my-value"
# Ler dados
wrangler kv key get --namespace-id=abc123 "my-key"
# Excluir dados
wrangler kv key delete --namespace-id=abc123 "my-key"
# Listar todas as chaves (com filtro por prefixo)
wrangler kv key list --namespace-id=abc123 --prefix="session:"
Esses comandos são úteis durante a depuração, mas, em produção, operar pelo código do Worker continua sendo mais eficiente.
TypeScript na prática
Finalmente chegamos ao código. A seguir, você verá exemplos completos e executáveis.
Operações CRUD básicas
Comecemos pelas operações básicas de criação, leitura, atualização e exclusão:
// src/index.ts
interface Env {
MY_KV: KVNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
// Gravar dados
if (path === "/put") {
const key = url.searchParams.get("key") || "default";
const value = url.searchParams.get("value") || "hello";
await env.MY_KV.put(key, value);
return new Response(`Saved: ${key} = ${value}`);
}
// Ler dados
if (path === "/get") {
const key = url.searchParams.get("key") || "default";
const value = await env.MY_KV.get(key);
if (value === null) {
return new Response("Key not found", { status: 404 });
}
return new Response(value);
}
// Excluir dados
if (path === "/delete") {
const key = url.searchParams.get("key") || "default";
await env.MY_KV.delete(key);
return new Response(`Deleted: ${key}`);
}
// Listar chaves (por prefixo)
if (path === "/list") {
const prefix = url.searchParams.get("prefix") || "";
const keys = await env.MY_KV.list({ prefix });
const keyList = keys.keys.map(k => k.name).join("\n");
return new Response(keyList || "No keys found");
}
return new Response("Try /put, /get, /delete, or /list");
},
};
Você pode executar esse código diretamente com o Wrangler:
wrangler dev
# Testar a gravação
curl "http://localhost:8787/put?key=test&value=helloworld"
# Testar a leitura
curl "http://localhost:8787/get?key=test"
Implementação completa de Session Storage
Este é um dos usos mais comuns do KV. Veja uma implementação completa para gerenciamento de sessões:
// src/session.ts
interface SessionData {
userId: string;
email: string;
createdAt: number;
expiresAt: number;
}
interface Env {
SESSION_KV: KVNamespace;
}
const SESSION_TTL = 3600; // Expira em 1 hora
class SessionManager {
private kv: KVNamespace;
constructor(kv: KVNamespace) {
this.kv = kv;
}
// Criar uma sessão
async create(userId: string, email: string): Promise<string> {
const sessionId = crypto.randomUUID();
const sessionData: SessionData = {
userId,
email,
createdAt: Date.now(),
expiresAt: Date.now() + SESSION_TTL * 1000,
};
// Gravar no KV com TTL (expiração automática)
await this.kv.put(
`session:${sessionId}`,
JSON.stringify(sessionData),
{ expirationTtl: SESSION_TTL }
);
return sessionId;
}
// Ler uma sessão
async get(sessionId: string): Promise<SessionData | null> {
const raw = await this.kv.get(`session:${sessionId}`);
if (!raw) return null;
try {
return JSON.parse(raw) as SessionData;
} catch {
return null;
}
}
// Excluir a sessão (logout)
async delete(sessionId: string): Promise<void> {
await this.kv.delete(`session:${sessionId}`);
}
// Renovar a sessão (estender a expiração)
async refresh(sessionId: string): Promise<boolean> {
const session = await this.get(sessionId);
if (!session) return false;
session.expiresAt = Date.now() + SESSION_TTL * 1000;
await this.kv.put(
`session:${sessionId}`,
JSON.stringify(session),
{ expirationTtl: SESSION_TTL }
);
return true;
}
}
// Ponto de entrada do Worker
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const sessionManager = new SessionManager(env.SESSION_KV);
const url = new URL(request.url);
// Login (criar sessão)
if (url.pathname === "/login" && request.method === "POST") {
const body = await request.json();
const sessionId = await sessionManager.create(
body.userId as string,
body.email as string
);
return new Response(JSON.stringify({ sessionId }), {
headers: { "Content-Type": "application/json" },
});
}
// Validar a sessão
if (url.pathname === "/verify") {
const sessionId = url.searchParams.get("sessionId");
if (!sessionId) {
return new Response("Missing sessionId", { status: 400 });
}
const session = await sessionManager.get(sessionId);
if (!session) {
return new Response("Session not found", { status: 401 });
}
return new Response(JSON.stringify(session), {
headers: { "Content-Type": "application/json" },
});
}
// Logout
if (url.pathname === "/logout") {
const sessionId = url.searchParams.get("sessionId");
if (sessionId) {
await sessionManager.delete(sessionId);
}
return new Response("Logged out");
}
return new Response("Not found", { status: 404 });
},
};
Alguns pontos importantes:
- Expiração automática por TTL: o parâmetro
expirationTtlfaz o KV remover dados expirados, sem limpeza manual - Prefixo de chave: usar
session:como prefixo facilita consultas em lote e separa tipos diferentes de dados - Serialização JSON: KV armazena apenas strings, portanto objetos complexos precisam de
JSON.stringifyeJSON.parse
Implementação de cache de respostas de API
Outro cenário comum é armazenar respostas de APIs de terceiros para reduzir o número de chamadas e a latência.
// src/api-cache.ts
interface Env {
CACHE_KV: KVNamespace;
}
const DEFAULT_CACHE_TTL = 300; // Cache por 5 minutos
async function cachedFetch(
kv: KVNamespace,
cacheKey: string,
url: string,
ttl: number = DEFAULT_CACHE_TTL
): Promise<Response> {
// Tentar ler primeiro do cache
const cached = await kv.get(cacheKey, "text");
if (cached) {
console.log(`Cache hit: ${cacheKey}`);
return new Response(cached, {
headers: {
"Content-Type": "application/json",
"X-Cache": "HIT",
},
});
}
// Cache miss: chamar a API real
console.log(`Cache miss: ${cacheKey}`);
const response = await fetch(url);
const body = await response.text();
// Gravar no cache (usar cacheTtl para otimizar a leitura)
await kv.put(cacheKey, body, {
expirationTtl: ttl,
// cacheTtl mantém o cache de borda por mais tempo e reduz acessos à origem
});
return new Response(body, {
headers: {
"Content-Type": "application/json",
"X-Cache": "MISS",
},
});
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const apiUrl = url.searchParams.get("api");
if (!apiUrl) {
return new Response("Missing api parameter", { status: 400 });
}
// Usar a URL da API como chave do cache
const cacheKey = `api:${apiUrl}`;
return cachedFetch(env.CACHE_KV, cacheKey, apiUrl);
},
};
Otimização com o parâmetro cacheTtl
Este é um dos parâmetros mais ignorados na otimização de desempenho do KV.
cacheTtl controla por quanto tempo o dado permanece no cache de borda. O padrão é 60 segundos. Durante esse período, leituras repetidas da mesma chave são atendidas diretamente pelo cache de borda, sem chegar à origem.
Para dados muito acessados, você pode aumentar o cacheTtl:
// Para configurações muito acessadas, use um cache de borda mais longo
await env.MY_KV.get("config:feature-flags", {
cacheTtl: 3600, // Cache de borda por 1 hora
});
Assim, mesmo que os dados no central store do KV não mudem, o nó de borda os manterá por uma hora. Isso é muito útil para feature flags cujas alterações não precisam entrar em vigor imediatamente.
KV vs D1 vs R2 — guia de decisão para armazenamento
Confesso que essa escolha também me deixou em dúvida no início. Com tantas opções de armazenamento na Cloudflare, qual delas usar?
Veja uma árvore de decisão:
Árvore de decisão por cenário
Qual é o tipo dos seus dados?
├─ Precisa armazenar arquivos (imagens, vídeos, PDF)?
│ └─ SIM → R2
│
├─ Precisa de consultas SQL (usuários, pedidos, joins)?
│ └─ SIM → D1
│
├─ É chave-valor simples, com muitas leituras e poucas gravações?
│ ├─ Frequência de gravação > 1 RPS/chave?
│ │ └─ SIM → KV não é adequado; considere D1 ou Durable Objects
│ │
│ └─ NÃO → KV ✓
│
├─ Precisa de consistência imediata?
│ └─ SIM → Durable Objects
│ └─ NÃO → KV pode funcionar
│
└─ Ainda não sabe?
└─ Comece com KV; se for suficiente, não precisa trocar
Tabela comparativa detalhada
| Critério | KV | D1 | R2 |
|---|---|---|---|
| Modelo de dados | Chave-valor | SQL (SQLite) | Object Storage |
| Capacidade de consulta | Apenas get/put/delete | Consultas SQL completas | Sem consultas; somente caminhos |
| Limite de gravação | 1 RPS por chave | Sem limite rígido | Sem limite rígido |
| Latência de leitura | 500 µs-10 ms (hot) | 50-200 ms (depende da localização) | Rápida para download |
| Consistência | Eventual | Forte (região única) | Eventual |
| Tamanho máximo do value | 25 MB | Limite de linha do SQLite | 5 TB por arquivo |
| Franquia gratuita | 100 mil leituras/dia | 5 GB de armazenamento + 25 milhões de linhas lidas | 10 GB de armazenamento |
| Cenários típicos | Sessão, cache, configuração | Dados de usuários, pedidos, relatórios | Arquivos, imagens, backups |
Recomendações para cenários específicos
Autenticação de usuários / sessão
→ KV
Motivo: dados de sessão são chave-valor simples, lidos com frequência em cada solicitação e gravados poucas vezes, apenas no login e logout. Frameworks de autenticação como OpenAuth usam KV por padrão.
// session:userId → dados da sessão
await env.SESSION_KV.put(`session:${sessionId}`, JSON.stringify(session));
Perfis de usuário / gerenciamento de pedidos
→ D1
Motivo: é necessário fazer consultas SQL, como “buscar todos os pedidos de um usuário” ou “calcular as vendas do mês anterior”. O modelo get/put do KV não atende a esse tipo de consulta.
-- D1 permite consultas complexas
SELECT * FROM orders WHERE user_id = ? AND created_at > ?
Armazenamento de imagens e arquivos
→ R2
Motivo: arquivos são grandes demais — 25 MB é o limite do KV — e não precisam do padrão de leitura rápida de chave-valor. R2 é mais adequado para object storage.
// Armazenar um arquivo no R2
await env.MY_BUCKET.put("images/profile.jpg", imageBuffer);
Rate limiting de API
→ KV (com cuidado)
Motivo: contadores são chave-valor, mas a frequência de gravação pode ultrapassar 1 RPS. Para algo simples, como “verificar o número de solicitações de hoje”, KV ainda funciona; para rate limiting preciso por segundo, talvez você precise de Durable Objects ou Upstash Redis.
// Rate limiting simples (reiniciado diariamente)
const count = parseInt(await env.KV.get(`rate:${userId}`) || "0");
if (count > 100) {
return new Response("Rate limit exceeded", { status: 429 });
}
await env.KV.put(`rate:${userId}`, String(count + 1));
Cache de API de terceiros
→ KV
Motivo: o retorno da API é lido muitas vezes e gravado apenas quando o cache expira ou a chamada muda. Um cache com TTL de cinco minutos não exige consistência imediata.
Exemplo de uso combinado
Muitos projetos usam vários tipos de armazenamento em conjunto:
interface Env {
SESSION_KV: KVNamespace; // Sessões de usuários
CACHE_KV: KVNamespace; // Cache de API
DATABASE_D1: D1Database; // Dados de usuários e pedidos
FILES_R2: R2Bucket; // Arquivos enviados por usuários
}
// Uma única solicitação pode usar todos eles:
// 1. Ler a sessão em SESSION_KV
// 2. Ler o cache de uma API de terceiros em CACHE_KV
// 3. Consultar pedidos do usuário em DATABASE_D1
// 4. Retornar o avatar do usuário por FILES_R2
É nessa combinação que o ecossistema completo da Cloudflare mostra seu verdadeiro potencial.
Técnicas práticas de otimização de desempenho
Quando bem usado, KV funciona muito bem; quando mal usado, pode virar um gargalo. A seguir estão algumas otimizações que funcionaram nos meus testes.
1. Ajuste do parâmetro cacheTtl
O cacheTtl padrão é 60 segundos. Para dados muito acessados, aumentar esse valor pode trazer uma grande melhoria.
// ❌ Comportamento padrão: cache de borda por 60 segundos
await env.KV.get("config:feature-flags");
// ✅ Otimização: manter dados de configuração em cache por mais tempo
await env.KV.get("config:feature-flags", {
cacheTtl: 3600, // Cache de borda por 1 hora
});
Quando vale a pena aumentar o cacheTtl?
- Feature flags: esperar alguns minutos para uma configuração entrar em vigor é aceitável
- Configuração estática: endpoint de API e URL de serviços de terceiros
- Regras de redirecionamento: mapas de URL que mudam pouco
Quando não vale a pena?
- Dados de sessão: o estado de login precisa aparecer imediatamente
- Contadores em tempo real: o contador de rate limiting precisa ser preciso
2. Chamadas de API paralelas, não sequenciais
Esta é uma armadilha comum. Se o Worker precisa ler várias chaves, não faça uma leitura por vez.
// ❌ Leitura sequencial: cada chamada espera a anterior terminar
const user = await env.KV.get(`user:${userId}`);
const settings = await env.KV.get(`settings:${userId}`);
const permissions = await env.KV.get(`permissions:${userId}`);
// Latência total = 3 × latência de uma chamada
// ✅ Leitura paralela: as três chamadas começam juntas
const [user, settings, permissions] = await Promise.all([
env.KV.get(`user:${userId}`),
env.KV.get(`settings:${userId}`),
env.KV.get(`permissions:${userId}`),
]);
// Latência total ≈ a chamada mais lenta
As chamadas da API do KV são assíncronas e não bloqueiam a execução do Worker. Com Promise.all, você reduz a latência de várias solicitações à duração da mais lenta.
Nos meus testes, ler três chaves de forma sequencial levou cerca de 20 ms; em paralelo, apenas 8 ms.
3. Estratégia para hot keys
O desempenho do KV depende bastante de a chave estar “hot”, ou seja, ser acessada com frequência.
Estratégia para evitar cold keys:
// ❌ Chaves muito dispersas; cada usuário acessa apenas a própria chave
await env.KV.get(`session:${userId}`); // Só este usuário acessa: cold key
// ✅ Consolidar em uma hot key (para dados compartilhados)
await env.KV.get("config:global-flags"); // Compartilhada por todos: hot key
Isso não significa colocar todos os dados em uma única chave. O correto é:
- Dados privados do usuário: uma chave por ID de usuário, como sessão e perfil
- Dados globais compartilhados: uma única hot key, como configurações, flags e regras de redirecionamento
4. Boas práticas para organizar namespaces
Uma conta pode ter 1.000 namespaces. Use esse limite para separar diferentes tipos de dados.
# wrangler.toml
[[kv_namespaces]]
binding = "SESSION_KV"
id = "xxx" # Sessões de usuários
[[kv_namespaces]]
binding = "CACHE_KV"
id = "yyy" # Cache de API
[[kv_namespaces]]
binding = "CONFIG_KV"
id = "zzz" # Dados de configuração
Vantagens:
- Limpeza isolada: você pode limpar
CACHE_KVem lote sem afetarSESSION_KV - Estratégias de TTL diferentes: TTL curto para
SESSIONe longo paraCONFIG - Monitoramento separado: o Cloudflare Dashboard mostra o uso de cada namespace separadamente
5. Técnicas para operações em lote
KV oferece a operação list(), que busca chaves por prefixo:
// Listar todas as chaves de sessão
const result = await env.SESSION_KV.list({ prefix: "session:" });
// result.keys é um array
for (const key of result.keys) {
console.log(key.name);
}
// Se houver muitas chaves, a paginação usa um cursor
if (!result.list_complete) {
const next = await env.SESSION_KV.list({
prefix: "session:",
cursor: result.cursor,
});
}
Para remover sessões expiradas em lote:
// Remover todas as sessões (use com cuidado)
const keys = await env.SESSION_KV.list({ prefix: "session:" });
for (const key of keys.keys) {
await env.SESSION_KV.delete(key.name);
}
Atenção: exclusões em lote precisam ser feitas com cuidado, pois consomem muitas operações de gravação.
Preços e limites — guia para controlar custos
Os preços do KV são acessíveis, mas alguns limites exigem atenção. Se você os ultrapassar, a operação poderá falhar diretamente.
Free Plan vs Paid Plan
| Métrica | Free Plan | Paid Plan (US$ 5/mês) |
|---|---|---|
| Leituras | 100.000/dia | Ilimitadas (cobradas por uso) |
| Gravações | 1.000/dia | Ilimitadas (cobradas por uso) |
| Exclusões | 1.000/dia | Ilimitadas (cobradas por uso) |
| Listagens | 1.000/dia | Ilimitadas (cobradas por uso) |
| Armazenamento | 1 GB | Ilimitado (cobrado por uso) |
| Número de namespaces | 1.000 | 1.000 |
O Free Plan é mais do que suficiente para projetos pessoais e testes. Para produção, vale considerar o Paid Plan. Por US$ 5 ao mês, você recebe:
- Sem limite fixo de leitura; o consumo é cobrado por uso
- Mais capacidade de gravação
- Monitoramento e alertas no Dashboard
Write Rate Limit: a restrição mais importante
Este é o limite mais importante do KV e também a armadilha mais comum:
Cada chave única pode receber no máximo uma gravação por segundo (1 RPS).
Se ultrapassar esse limite, a solicitação retorna um erro.
// ❌ Gravações em alta frequência falharão
for (let i = 0; i < 10; i++) {
await env.KV.put("counter", String(i)); // A partir da segunda, falha
}
// ✅ Distribuir os valores entre chaves evita o limite
await env.KV.put(`counter:${Math.floor(Date.now() / 1000)}`, value);
// Uma chave nova por segundo não aciona o limite
Qual é a razão desse limite? A arquitetura do KV é “gravar uma vez, replicar no mundo inteiro”. Gravações frequentes na mesma chave fariam o custo da replicação crescer demais. A Cloudflare usa essa restrição para proteger o sistema.
Formas de lidar com o limite:
- Distribuir a chave no tempo: use
counter:timestamppara criar uma chave nova a cada segundo - Distribuir com UUID: use um novo UUID como chave em cada gravação
- Usar D1 ou Durable Objects: se gravações frequentes forem indispensáveis
Limite de tamanho do value
O tamanho máximo de um value no KV é 25 MB; no início de 2025, esse limite aumentou de 10 MB.
// ❌ Acima de 25 MB ocorre um erro
const largeData = generateBigString(30_000_000); // 30 MB
await env.KV.put("large-key", largeData); // Error!
// ✅ Use R2 para dados grandes
await env.R2_BUCKET.put("large-key", largeData);
Para sessões, configurações e cache, 25 MB é mais do que suficiente. Se você precisa guardar arquivos ou JSONs muito grandes, R2 é uma opção melhor.
Estratégia de gerenciamento de namespaces
Cada conta pode ter até 1.000 namespaces. O aumento de 200 para 1.000, no início de 2025, mostra que a Cloudflare flexibilizou essa restrição.
Estratégia de organização:
// Agrupar por função
SESSION_KV // Sessões de usuários
CACHE_KV // Cache de API
CONFIG_KV // Dados de configuração
RATE_LIMIT_KV // Contadores de rate limiting
E se os namespaces acabarem? Você pode usar prefixos de chave para separar dados dentro do mesmo namespace:
// Separação dentro de um único namespace
await env.KV.put("session:user1", data);
await env.KV.put("cache:api1", data);
await env.KV.put("config:flags", data);
Fórmula para estimar custos
Se o seu projeto usa o Paid Plan:
Custo mensal = US$ 5 (taxa básica) + custo de leitura + custo de gravação + custo de armazenamento
Custo de leitura = número de leituras × US$ 0,01 / 100.000
Custo de gravação = número de gravações × US$ 1,00 / 1.000.000
Custo de armazenamento = tamanho armazenado × US$ 0,50 / GB
Exemplo para um projeto com 100 mil solicitações por dia:
- Leituras: 100.000 × 30 = 3 milhões de leituras/mês = US$ 0,30
- Gravações: supondo 1.000 × 30 = 30 mil gravações/mês ≈ US$ 0,03
- Armazenamento: 10 MB × US$ 0,50/GB ≈ US$ 0,005
- Custo total: US$ 5 + US$ 0,33 ≈ US$ 5,35/mês
É barato, não é? Esse é o estilo típico de preços da Cloudflare.
Conclusão
Depois de tudo isso, a ideia central cabe em uma frase: KV é a “memória portátil” dos Workers, ideal para sessões, cache e configurações com muitas leituras e poucas gravações.
Aqui está uma lista rápida para decidir:
Use KV se:
- Os dados são pares chave-valor simples
- A frequência de leitura é muito maior do que a de gravação
- Você não precisa de consistência imediata
- Cada chave recebe no máximo uma gravação por segundo
Troque por D1 se:
- Você precisa de consultas SQL
- Existem relações complexas entre tabelas
- A frequência de gravação pode ultrapassar 1 RPS
Troque por R2 se:
- Você armazena arquivos, imagens ou vídeos
- O value ultrapassa 25 MB
Troque por Durable Objects se:
- Você precisa de consistência imediata
- O caso envolve edição colaborativa ou sincronização em tempo real
O próximo passo? Conecte KV ao seu projeto Workers. Comece pelo session storage: o código acima pode ser executado diretamente. Se encontrar algum problema, a documentação oficial da Cloudflare é bem detalhada; você também pode consultar os outros artigos desta série.
Se D1 ou R2 também interessam a você, confira os demais artigos da série cloudflare-bindui. Nela, apresentarei toda a matriz de armazenamento em detalhes.
FAQ
Qual é o limite de gravação do Cloudflare Workers KV?
KV é adequado para armazenar sessões de usuários?
Qual é a diferença entre KV e D1? Qual devo escolher?
• KV: modelo chave-valor, latência de 500 µs a 10 ms para hot keys e limite de gravação de 1 RPS por chave
• D1: modelo SQL baseado em SQLite, com consultas complexas e sem limite rígido de gravação
Escolha D1 quando precisar de consultas SQL; para chave-valor simples com muitas leituras e poucas gravações, escolha KV.
Por que a latência do KV pode chegar a 500 µs-10 ms?
Para que serve o parâmetro cacheTtl?
Qual é o tamanho máximo de um value no KV?
22 min de leitura · Publicado em: 22 abr 2026 · Atualizado em: 4 set 2026
Cloudflare Full Stack
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Guia completo para implantar Astro na Cloudflare: SSR e acesso 3 vezes mais rápido na China
Aprenda a implantar Astro na Cloudflare Pages do zero, configurar os três modos do adaptador SSR e otimizar o acesso na China com IP selecionado, CNAME e DNS por rota, reduzindo a latência em até 3 vezes.
Parte 18 de 23
Próximo
Cloudflare Dynamic Workers: o segredo do sandbox de IA 100 vezes mais rápido que contêineres
Cloudflare Dynamic Workers usa V8 Isolates para criar sandboxes de AI Agent com inicialização 100 vezes mais rápida que contêineres e eficiência de memória de 10 a 100 vezes maior. Este guia analisa arquitetura, segurança, API prática e custo-benefício.
Parte 20 de 23



Comentários
Entre com GitHub para comentar