Cloudflare
KV
Cloudflare KV
Feature Flags
Rate Limiting
Configuração
Edge

KV for rate limiting, feature flags and distributed configuration: where it works and where it breaks

Technical analysis of three common Cloudflare KV use cases: rate limiting (does not work), feature flags (works with caveats), and distributed configuration (best use case).

KV for rate limiting, feature flags and distributed configuration: where it works and where it breaks

The temptation to use KV for rate limiting is understandable. You already have the KV in the binding, it is global, and rate limiting seems simple: increment a counter per IP key and reject it when it passes the limit. The problem is that this implementation doesn't work — and the flaw isn't subtle enough to show up in testing.

KV does not have atomic operations. There is no compare-and-swap, there is no atomic increment. When two Workers run simultaneously for the same IP, they both make get of the counter — getting, say, the value 5 — they both make put with the value 6, and one of the increments is lost. Under real traffic, the incremental loss rate grows with competition. The rate limiter counts less than it should, and requests that should be blocked pass through.

This is not an implementation bug that you solve with retry. It is the direct consequence of the absence of synchronization primitives in KV. The architecture was designed for another type of workload.

Why rate limiting needs atomicity

A rate limiting counter needs to guarantee that the read-increment-write sequence is atomic. If two processes execute this sequence simultaneously over the same counter, the correct result is the original value plus two. Without atomicity, the result is often the original value plus one.

Cloudflare has two solutions to this problem. The first is the native Rate Limiting functionality, configurable via rules in the dashboard or via Rulesets API, which operates below the Worker level and uses internal infrastructure with the correct synchronization guarantees. The second is Durable Objects, which offers an isolate with persistent state and serialized access — you can implement an exact counter because only one Worker at a time runs inside the Durable Object for that key.

KV is not part of the solution for exact rate limiting. Attempts to implement rate limiting with KV end up in systems that reject less traffic than they should, exactly at peak times where rate limiting matters most.

Feature flags with KV: what works and what doesn't

Feature flags are the most cited use case for KV, and they work well within the correct limits. The basic model is simple: you write a JSON object in a key with all the system flags, and each Worker reads this key to decide the behavior.

// 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) { /* ... */ }

The model works because the workload is read-heavy with write rarely. A flag changes a few times a week. It is read by each request from each Worker in each PoP. The read to write ratio is excellent for KV.

The real limitation is the 60 second propagation. Enabling a flag does not activate it for all users simultaneously — there is a window where part of the PoPs serve the old behavior and part serve the new. For most feature flags this is tolerable. For a security critical rollout where you need a flag to reach all users at the same time, it is a real operational constraint.

Where the model breaks down is in dynamic flag evaluation per user. If you need to evaluate flags based on user attributes — subscription plan, A/B test group, region, specific entity — the global flags JSON doesn't carry that logic. You need a lookup per user, which usually means a call to D1 or an external service. The KV remains as a global config cache, not as a complete flag system.

For gradual rollouts by percentage of users (10% see the feature), implementation with KV requires you to code the sampling logic in the Worker and use KV only to store the target percentage. The sampling itself is stateless — done in the Worker based on the user ID hash — so the KV is correctly used as a configuration store.

Distributed configuration: the best use case for KV

If feature flags are a good use case, distributed configuration is the ideal use case. The difference is one of granularity and frequency of change.

Application configuration changes by deliberate operation action: external service endpoint update, timeout adjustment, list of allowed IPs, business parameters. These changes happen very infrequently — hours or days between updates — and need to be read for each request.

The module-level caching pattern extracts the most from KV in this case. Variables in a Worker's module scope persist as long as the isolate is active, potentially for thousands of 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; // ... } };

The first request for each isolate reads the KV. All subsequent requests from the same isolate use the in-memory value — zero KV latency, zero read operations charged. A configuration update propagates to new isolates as existing isolates are evicted by the runtime.

The effective propagation time is no longer the 60 seconds of global KV propagation — it is 60 seconds plus the lifetime of the active isolates. Long-lived isolates can carry old configuration for longer. For most operational changes, this is acceptable. For emergency situations that require immediate propagation, you can force a restart of Workers via API.

The cost of operation in each scenario

For the three use cases, the KV cost model creates different pressures. Rate limiting would be writing per request — unfeasible for any volume. Feature flags have a minimum writing cost and reading cost that depends on how many Workers are reading the key in how many PoPs per second. With module-level caching, even feature flags read by millions of requests can consume surprisingly few read operations — an isolate serving 10,000 requests reads the KV once.

Distributed configuration with module-level caching is the lowest possible operational cost scenario in KV. One write per config change, one read per isolate per restart — the monthly cost of KV operations for this pattern is negligible even in high-traffic applications.

What do these three cases reveal about KV

Analysis of rate limiting, feature flags, and distributed configuration makes clear the frontier of KV: data that you write rarely and read often, where an inconsistency window of up to 60 seconds is tolerable, and where you don't need atomicity.

When any of these three conditions are not met, KV will produce either silent bugs (no atomicity), or unacceptable inconsistency (propagation window), or prohibitive cost (high write frequency). Recognizing this boundary before implementing saves the debugging session that is often the most expensive way to learn where a tool doesn't apply.

Also read