Alternar tema

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

Easton editorial illustration: bottleneck pressure gauge

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.

500 µs - 10 ms
Faixa de latência de hot keys

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 armazenamentoModelo de dadosMelhor usoLimite de gravaçãoCaracterística de latência
KVChave-valorSessão, cache, configuração1 RPS/chavehot keys: 500 µs-10 ms
D1SQL (SQLite)Dados de usuários, pedidos, relatóriosSem limite rígidoDepende da localização, normalmente 50-200 ms
R2Object StorageArquivos, imagens, vídeosSem limite rígidoDownload rápido; upload depende do tamanho do arquivo
Durable ObjectsObjetos com estadoEdição colaborativa, WebSocketSem limite rígidoPrecisa 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.

30%
Taxa de acerto do cache de borda

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:

  1. Conexão direta entre Workers e KV, sem passar pela antiga camada Front Line
  2. 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:

  1. Expiração automática por TTL: o parâmetro expirationTtl faz o KV remover dados expirados, sem limpeza manual
  2. Prefixo de chave: usar session: como prefixo facilita consultas em lote e separa tipos diferentes de dados
  3. Serialização JSON: KV armazena apenas strings, portanto objetos complexos precisam de JSON.stringify e JSON.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 &gt; 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
500 µs-10 ms
Latência de hot keys no KV
50-200 ms
Latência do D1
25 MB
Tamanho máximo de um value no KV
Source: Documentação oficial da Cloudflare

Tabela comparativa detalhada

CritérioKVD1R2
Modelo de dadosChave-valorSQL (SQLite)Object Storage
Capacidade de consultaApenas get/put/deleteConsultas SQL completasSem consultas; somente caminhos
Limite de gravação1 RPS por chaveSem limite rígidoSem limite rígido
Latência de leitura500 µs-10 ms (hot)50-200 ms (depende da localização)Rápida para download
ConsistênciaEventualForte (região única)Eventual
Tamanho máximo do value25 MBLimite de linha do SQLite5 TB por arquivo
Franquia gratuita100 mil leituras/dia5 GB de armazenamento + 25 milhões de linhas lidas10 GB de armazenamento
Cenários típicosSessão, cache, configuraçãoDados de usuários, pedidos, relatóriosArquivos, 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 &gt; ?

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 &gt; 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.

60%
Redução de latência (paralelo vs sequencial)
Source: Dados de testes práticos

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:

  1. Limpeza isolada: você pode limpar CACHE_KV em lote sem afetar SESSION_KV
  2. Estratégias de TTL diferentes: TTL curto para SESSION e longo para CONFIG
  3. 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.

100.000
Leituras gratuitas/dia
1.000
Gravações gratuitas/dia
1 GB
Armazenamento gratuito
US$ 5
Mensalidade do plano pago
Source: Página de preços da Cloudflare

Free Plan vs Paid Plan

MétricaFree PlanPaid Plan (US$ 5/mês)
Leituras100.000/diaIlimitadas (cobradas por uso)
Gravações1.000/diaIlimitadas (cobradas por uso)
Exclusões1.000/diaIlimitadas (cobradas por uso)
Listagens1.000/diaIlimitadas (cobradas por uso)
Armazenamento1 GBIlimitado (cobrado por uso)
Número de namespaces1.0001.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 &lt; 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:

  1. Distribuir a chave no tempo: use counter:timestamp para criar uma chave nova a cada segundo
  2. Distribuir com UUID: use um novo UUID como chave em cada gravação
  3. 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?
Cada chave única pode receber no máximo uma gravação por segundo (1 RPS). Se esse limite for ultrapassado, a solicitação retorna um erro. Para contornar isso, distribua as chaves usando timestamps, como counter:timestamp, ou use D1 ou Durable Objects.
KV é adequado para armazenar sessões de usuários?
Sim. Dados de sessão são pares chave-valor simples, lidos com frequência a cada validação de solicitação e gravados poucas vezes, normalmente apenas no login e logout. Com TTL, eles expiram automaticamente, sem limpeza manual.
Qual é a diferença entre KV e D1? Qual devo escolher?
A diferença principal é o modelo de dados:

• 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?
Por causa da arquitetura de cache em três camadas: Edge Cache (nó de borda) → Regional Cache → Central Store. Cerca de 30% das solicitações são atendidas diretamente pelo cache de borda, sem acessar a origem. Após as otimizações da Cloudflare em 2025, o serviço ficou três vezes mais rápido.
Para que serve o parâmetro cacheTtl?
Ele controla por quanto tempo os dados permanecem no cache de borda. O padrão é 60 segundos. Para dados muito acessados, como feature flags e configurações, você pode defini-lo como 3600 segundos, ou uma hora, mantendo o cache por mais tempo nos nós de borda e reduzindo acessos à origem.
Qual é o tamanho máximo de um value no KV?
25 MB; o limite aumentou de 10 MB no início de 2025. Valores maiores geram erro. Para dados grandes, como imagens e vídeos, use o R2 Object Storage.

22 min de leitura · Publicado em: 22 abr 2026 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog