Using Cloudflare Workers KV for Feature Flags Without the Origin Round-Trip
Feature-flag lookups don't need an origin round-trip. Here's how to serve them from Cloudflare Workers KV, with a worked routing example and the consistency trade-offs you must design around.
14 Aug 2026, 11:38 UTC

Every request that has to ask your origin "is this feature on?" pays for that question in latency. If your app is served from one region and your users are everywhere, a feature-flag lookup can add tens to hundreds of milliseconds before you even start rendering. The useful trick: move the flags to the edge with Workers KV, read them inside a Cloudflare Worker, and never touch the origin for configuration at all.
The thesis is simple: KV is a mediocre database but an excellent configuration store. If you treat it that way — high-read, low-write, tolerant of brief staleness — it removes a whole class of origin calls.
What KV actually gives you
Workers KV is a globally replicated, eventually consistent key-value store that your Worker can read in a few milliseconds because values are cached at the edge location serving the request. Writes go to a central location and propagate outward; Cloudflare documents that propagation can take up to about 60 seconds, and values may be served stale from edge caches in the meantime.
That profile dictates the use cases:
- Good fit: feature flags, A/B test assignments, maintenance-mode switches, redirect tables, small JSON config blobs.
- Bad fit: shopping carts, session state that must be read-your-writes, counters, anything updated more than occasionally or read immediately after writing.
A worked example: a feature-flag Worker
Assume a KV namespace bound as FLAGS, with keys like flag:checkout-v2 containing {"enabled": true, "rollout": 25}. The Worker reads the flag and routes accordingly:
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname.startsWith("/checkout")) {
// cached: true lets the edge serve a cached read (faster, possibly staler)
const raw = await env.FLAGS.get("flag:checkout-v2", {
type: "json",
cacheTtl: 60,
});
const flag = raw ?? { enabled: false, rollout: 0 };
if (flag.enabled && inRollout(request, flag.rollout)) {
url.pathname = "/v2" + url.pathname;
return fetch(url.toString(), request);
}
}
return fetch(request);
},
};
function inRollout(request, pct) {
const id = request.headers.get("cf-ray") ?? "";
let h = 0;
for (const c of id) h = (h * 31 + c.charCodeAt(0)) % 100;
return h < pct;
}Two details matter here. First, cacheTtl: 60 tells the edge it may serve a cached read for 60 seconds, which makes repeated reads extremely cheap and fast at the cost of more staleness. Second, the flag lookup happens before the origin fetch, so a disabled flag costs one edge read and nothing else.
To set a flag, use Wrangler from your machine (requires a Cloudflare API token with KV edit permissions):
npx wrangler kv key put --namespace-id=<NAMESPACE_ID> \
"flag:checkout-v2" '{"enabled":true,"rollout":25}'Find your namespace ID in the dashboard under Workers & Pages → KV, or with npx wrangler kv namespace list. Don't expect the new value to be visible everywhere instantly — that's the next section.
The trade-off you have to design around
Eventual consistency is the whole game. After you flip a flag, some edge locations may serve the old value for up to a minute (longer if you set a high cacheTtl). For a gradual rollout that's fine. For a kill switch — "turn off the broken payment provider now" — a 60-second-plus delay may be unacceptable.
Practical mitigations:
- Keep
cacheTtllow (or omit it) for flags you might need to kill quickly; accept the slightly slower reads. - For true emergency switches, consider a different mechanism — a Worker environment variable you redeploy, or Cloudflare's tighter-consistency options like Durable Objects — and reserve KV for flags where a minute of lag is harmless.
- Design flags to fail safe: if the KV read returns null or throws, default to the old behavior, as the example does with
?? { enabled: false }.
Also watch the limits. Workers have CPU-time caps per request (the exact number depends on your plan and is worth checking in current docs), and KV has per-plan read/write quotas. A flag read per request is well within normal usage, but a Worker that reads dozens of keys per request on heavy traffic can hit throttling or a surprising bill. The dashboard's Workers & Pages analytics show KV operation counts — check them after your first week of real traffic.
How to verify it behaves the way you think
Don't take the consistency model on faith; measure it once:
- Deploy the Worker with
npx wrangler deployand confirm a flag read works viacurl -I https://<your-worker>.workers.dev/checkout. - Write a flag value with Wrangler, then immediately request the Worker through a VPN endpoint in another region. Note how long until the new value appears — that's your real-world propagation time.
- Temporarily point the binding at an empty namespace and confirm the fail-safe default path works (old behavior served, no 500s).
Closing
KV earns its place when you stop asking it to be a database. Put your slow-changing, read-everywhere configuration in it, read it at the edge with an explicit staleness budget, and keep genuinely urgent switches on a mechanism with tighter guarantees. Start with one low-risk flag, measure propagation from a second region, and watch the KV metrics in the dashboard before you migrate the rest.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.