A tentação de usar o KV para rate limiting é compreensível. Você já tem o KV no binding, ele é global, e rate limiting parece simples: incrementar um contador por chave de IP e rejeitar quando passa do limite. O problema é que essa implementação não funciona — e a falha não é sutil o suficiente para aparecer em testes.
O KV não tem operações atômicas. Não existe compare-and-swap, não existe incremento atômico. Quando dois Workers executam simultaneamente para o mesmo IP, os dois fazem get do contador — obtendo, digamos, o valor 5 — os dois fazem put com o valor 6, e um dos incrementos é perdido. Sob tráfego real, a taxa de perda de incrementos cresce com a concorrência. O rate limiter conta menos do que deveria, e requisições que deveriam ser bloqueadas passam.
Esse não é um bug de implementação que você resolve com retry. É a consequência direta da ausência de primitivas de sincronização no KV. A arquitetura foi projetada para outro tipo de workload.
Por que rate limiting precisa de atomicidade
Um contador de rate limiting precisa garantir que a sequência leitura-incremento-escrita seja atômica. Se dois processos executam essa sequência simultaneamente sobre o mesmo contador, o resultado correto é o valor original mais dois. Sem atomicidade, o resultado frequente é o valor original mais um.
A Cloudflare tem duas soluções para esse problema. A primeira é a funcionalidade nativa de Rate Limiting, configurável via regras no dashboard ou via Rulesets API, que opera abaixo do nível do Worker e usa infraestrutura interna com as garantias de sincronização corretas. A segunda é Durable Objects, que oferece um isolate com estado persistente e acesso serializado — você pode implementar um contador exato porque só um Worker por vez executa dentro do Durable Object para aquela chave.
KV não faz parte da solução para rate limiting exato. Tentativas de implementar rate limiting com KV acabam em sistemas que rejeitam menos tráfego do que deveriam, exatamente nos momentos de pico onde o rate limiting mais importa.
Feature flags com KV: o que funciona e o que não
Feature flags são o caso de uso mais citado para KV, e funcionam bem dentro dos limites corretos. O modelo básico é simples: você escreve um objeto JSON em uma chave com todos os flags do sistema, e cada Worker lê essa chave para decidir o comportamento.
// escrita (admin) await env.FLAGS.put('feature-flags', JSON.stringify({ newCheckout: true, betaSearch: false, darkMode: true })); // leitura (worker) const flags = await env.FLAGS.get('feature-flags', { type: 'json' }); if (flags.newCheckout) { /* ... */ }
O modelo funciona porque o workload é read-heavy com write raramente. Um flag muda algumas vezes por semana. É lido por cada request de cada Worker em cada PoP. A proporção de leitura para escrita é excelente para o KV.
A limitação real é a propagação de 60 segundos. Ativar um flag não ativa para todos os usuários simultaneamente — há uma janela onde parte dos PoPs serve o comportamento antigo e parte serve o novo. Para a maioria dos feature flags isso é tolerável. Para um rollout de segurança crítica onde você precisa que um flag chegue a todos os usuários ao mesmo tempo, é uma restrição operacional real.
Onde o modelo quebra é em flag evaluation dinâmica por usuário. Se você precisa avaliar flags com base em atributos do usuário — plano de assinatura, grupo de teste A/B, região, entidade específica — o JSON de flags global não carrega essa lógica. Você precisa de um lookup por usuário, o que geralmente significa uma chamada a D1 ou a um serviço externo. O KV fica como cache de config global, não como sistema de flag completo.
Para rollouts graduais por porcentagem de usuários (10% vê a feature), a implementação com KV requer que você codifique a lógica de sampling no Worker e use o KV apenas para armazenar a porcentagem alvo. O sampling em si é stateless — feito no Worker com base em hash do user ID — então o KV é usado corretamente como store de configuração.
Configuração distribuída: o melhor caso de uso do KV
Se feature flags são um bom caso de uso, configuração distribuída é o caso de uso ideal. A diferença é de granularidade e frequência de mudança.
Configuração de aplicação muda por ação deliberada de operação: atualização de endpoint de serviço externo, ajuste de timeout, lista de IPs permitidos, parâmetros de negócio. Essas mudanças acontecem com frequência muito baixa — horas ou dias entre atualizações — e precisam ser lidas por cada request.
O padrão com module-level caching extrai o máximo do KV para esse caso. Variáveis no escopo do módulo de um Worker persistem enquanto o isolate estiver ativo, potencialmente por milhares de requests:
let config = null; export default { async fetch(request, env, ctx) { config = config ?? await env.CONFIG.get('app-settings', { type: 'json' }); // config está disponível para todos os requests // sem read do KV após o primeiro const timeout = config.upstreamTimeoutMs; // ... } };
O primeiro request de cada isolate faz o read do KV. Todos os requests subsequentes do mesmo isolate usam o valor em memória — zero latência de KV, zero operações de leitura cobradas. Uma atualização de configuração propaga para novos isolates conforme os isolates existentes são evicted pelo runtime.
O tempo de propagação efetivo não é mais os 60 segundos de propagação global do KV — é 60 segundos mais o tempo de vida dos isolates ativos. Isolates com vida longa podem carregar configuração antiga por mais tempo. Para a maioria das mudanças operacionais, isso é aceitável. Para situações de emergência que exigem propagação imediata, você pode forçar um restart dos Workers via API.
O custo de operação em cada cenário
Para os três casos de uso, o modelo de custo do KV cria pressões diferentes. Rate limiting estaria escrevendo por request — inviável a qualquer volume. Feature flags têm custo de escrita mínimo e custo de leitura que depende de quantos Workers estão lendo a chave em quantos PoPs por segundo. Com module-level caching, mesmo feature flags lidos por milhões de requests podem consumir surpreendentemente poucas operações de leitura — um isolate que serve 10 mil requests lê o KV uma vez.
Configuração distribuída com module-level caching é o cenário de menor custo operacional possível no KV. Uma escrita por mudança de config, uma leitura por isolate por restart — o custo mensal de operações de KV para esse padrão é negligenciável mesmo em aplicações de alto tráfego.
O que esses três casos revelam sobre o KV
A análise de rate limiting, feature flags e configuração distribuída deixa clara a fronteira do KV: dados que você escreve raramente e lê muitas vezes, onde uma janela de inconsistência de até 60 segundos é tolerável, e onde você não precisa de atomicidade.
Quando qualquer uma dessas três condições não é atendida, o KV vai produzir ou bugs silenciosos (sem atomicidade), ou inconsistência inaceitável (janela de propagação), ou custo proibitivo (alta frequência de escrita). Reconhecer essa fronteira antes de implementar economiza a sessão de debugging que costuma ser o jeito mais caro de aprender onde uma ferramenta não se aplica.
Leia também
- Cloudflare KV: o que globalmente distribuído significa quando você precisa escrever
- Invalidação de cache no KV: o problema que ninguém resolve com elegância
- KV em produção: os padrões que funcionam e os que enganam no início
- WAF + Rate Limiting + Bot Management: a trifeta de proteção no edge
- O que só Workers faz, o que só Pages faz e onde os dois se encontram
- KV vs R2 vs Cache API: quando usar cada camada de armazenamento do Cloudflare