Cloudflare
KV
Cloudflare KV
Produção
Padrões
Cache
Limites

KV em produção: os padrões que funcionam e os que enganam no início

Padrões de uso do Cloudflare KV que funcionam em produção: metadata trick, module-level caching, index key para list(), e os anti-padrões que esgotam o free tier rapidamente.

Mil escritas por dia parece suficiente até você colocar qualquer coisa em produção com usuários de verdade. Um sistema de login que escreve um token de sessão no KV por autenticação esgota esse limite com meia hora de tráfego moderado. O primeiro confronto com os limites reais do KV raramente acontece em staging.

O free tier — 100 mil leituras e 1 mil escritas por dia — foi desenhado para refletir o modelo correto de uso: muita leitura, escrita mínima. Quando uma equipe usa KV de forma consistente com esse modelo, o free tier dura muito. Quando usa como banco de sessão ou store de estado por usuário, o limite aparece na primeira semana.

Os padrões que funcionam

O uso mais sólido do KV é o armazenamento de configuração de aplicação lida repetidamente e alterada raramente. Um JSON com flags de produto, parâmetros de negócio, listas de permissão, endpoints de terceiros — esse tipo de dado muda por ação administrativa, não por usuário. Uma escrita no KV propaga para todos os PoPs e serve milhões de requests sem custo relevante. Um time que escreve nessa chave dez vezes por mês e lê dez milhões de vezes fica dentro do free tier confortavelmente.

Caching de HTML renderizado segue o mesmo princípio. Um post de blog, uma página de produto, um resultado de busca que não muda por usuário — você renderiza uma vez, salva no KV com um TTL apropriado e serve direto do cache do PoP para qualquer request subsequente. O custo de renderização cai, a latência cai, e o número de escritas fica proporcional à frequência de atualização do conteúdo, não ao volume de tráfego.

Tokens de sessão com TTL também se encaixam, desde que a sessão seja write-once. Você escreve o token na autenticação — uma escrita por login — e lê em cada request subsequente. Se um usuário faz login uma vez e fica ativo por horas, a proporção leitura/escrita é excelente. O que quebra esse modelo é sessão com estado mutável: cada atualização de dado de sessão vira uma escrita, e o custo explode.

O truque com metadata

Cada chave no KV pode carregar até 1024 bytes de metadata JSON arbitrário, separado do valor em si. Esse campo é retornado pelo getWithMetadata() junto com o valor, em uma única operação — sem custo adicional de leitura.

A utilidade prática é armazenar junto ao valor informações que você precisaria parsear ou inferir de outra forma. Para um arquivo binário salvo no KV, o metadata pode conter Content-Type, ETag, data de criação, tamanho original e qualquer cabeçalho HTTP relevante. O Worker lê a chave, recebe valor e metadata em uma chamada, e monta a resposta HTTP com os cabeçalhos corretos sem nenhuma lógica adicional de lookup ou parse.

Isso também serve para versioning leve. Salve no metadata a versão ou timestamp da última atualização. Qualquer consumidor pode verificar se está lendo a versão esperada sem buscar um segundo dado.

O problema de performance do list()

list() é a operação mais cara do KV em termos de performance relativa, e é a que aparece com mais frequência em paths que não deveriam usá-la. Uma chamada a list() em um namespace com 100 mil chaves é lenta — a latência vai depender do tamanho do namespace e do cursor — e conta como uma operação de list, que tem cota separada: 1 mil operações por dia no free tier, $0.50 por milhão no paid.

O problema real é usar list() no hot path de um request. Se cada request precisa descobrir quais chaves existem para servir uma resposta, você colocou uma operação de administração de dados dentro do caminho crítico de performance.

A solução é manter uma chave de índice. Você escreve no KV uma chave como __index__ cujo valor é um JSON com a lista das chaves do namespace — ou apenas os identificadores necessários para a lógica. Quando o namespace muda, você atualiza o índice junto com a escrita principal. O custo é uma escrita extra por operação de escrita. O benefício é que qualquer leitura de índice é uma leitura comum, com latência de cache e sem os problemas de escala do list().

Esse padrão tem a limitação óbvia de que o índice precisa ser mantido sincronizado manualmente. Se você tem múltiplos Writers, a ausência de operações atômicas no KV cria uma janela de inconsistência no índice. Para namespaces com escrita única ou escrita controlada por um único Writer, o padrão funciona bem.

Module-level caching: a otimização que ninguém documenta explicitamente

Workers em Cloudflare rodam em isolates V8. Um mesmo isolate pode servir milhares de requests antes de ser evicted. Variáveis declaradas no escopo do módulo — fora do handler — persistem entre requests enquanto o isolate estiver ativo.

Isso cria uma oportunidade de otimização simples e efetiva para dados de configuração. Em vez de fazer env.CONFIG.get('settings') em cada request, você declara uma variável no escopo do módulo e só busca no KV quando ela ainda não foi inicializada:

let config = null; export default { async fetch(request, env) { config = config ?? await env.CONFIG.get('settings', { type: 'json' }); // usa config } };

O primeiro request do isolate faz a leitura no KV. Todos os requests subsequentes do mesmo isolate usam o valor em memória. Para um dado que muda raramente — configuração de produto, flags de feature — isso elimina a leitura de KV de quase todos os requests, reduzindo latência e consumo de operações de leitura.

A implicação é que uma atualização no KV não é refletida imediatamente em todos os Workers — cada isolate vai continuar usando o valor em cache até ser evicted. Para dados onde 60 segundos a alguns minutos de delay são aceitáveis, o tradeoff é excelente. Para dados que precisam de atualização imediata em todos os Workers, esse padrão não é adequado.

O que não levar para produção sem repensar

Usar KV como fila de jobs não funciona. Sem operações atômicas, dois Workers podem ler o mesmo job, processá-lo em duplicata e marcar como concluído de forma independente. O resultado é processamento duplicado sem mecanismo de detecção.

Salvar dados de usuário mutáveis por chave de usuário escala mal no limite de escritas. Uma aplicação com 10 mil usuários ativos por dia que atualiza dados de perfil mesmo que uma vez por sessão já está na ordem de grandeza do limite paid de 1 milhão de escritas mensais — e o custo por escrita a $0.50/milhão começa a aparecer quando você vai além.

Namespace com alta densidade de chaves e necessidade frequente de listagem são uma armadilha de performance. list() é lento em namespaces grandes e não deve estar no caminho de request. Se o seu caso de uso exige listagem frequente, o modelo de dados precisa mudar — seja com chaves de índice mantidas manualmente, seja com uma ferramenta diferente.

O que o free tier revela sobre o design

Os limites do free tier — 100 mil leituras para 1 mil escritas — são um design document disfarçado. A proporção 100:1 entre leituras e escritas não é arbitrária. Ela descreve o workload que o KV foi construído para servir. Qualquer uso que inverta ou aproxime essa proporção está fora do modelo de operação pretendido, e vai encontrar limitações de custo, de performance ou de consistência que não aparecem em testes com baixo volume.

Leia também