DevOps ·

Feature Flags at the Edge with Cloudflare Workers: A Practical System Design

Running feature flags at the edge can cut rollout latency and reduce blast radius, but only if you design for consistency, cache invalidation, and failure modes. This guide covers architecture, TypeScript implementation patterns, and production tradeoffs.

Feature Flags at the Edge with Cloudflare Workers: A Practical System Design

Feature flags are easy in a single-region backend. They become more interesting when you move decisions to the edge.

At edge scale, you care about three things at once:

  1. Speed: evaluate flag state close to the user
  2. Safety: roll out gradually and reverse quickly
  3. Consistency: avoid users bouncing between two different experiences

Most teams get two out of three.

This guide walks through a practical architecture for feature flags with Cloudflare Workers, including TypeScript patterns you can use in production.

Why evaluate flags at the edge

If your app serves global traffic, central flag evaluation adds avoidable latency:

  • Browser request reaches nearest POP quickly
  • POP forwards to origin region for flag lookup
  • Origin returns decision
  • POP serves response

Even when each hop is “fast,” the sum can move your p95.

Edge evaluation removes that round trip for read-time decisions. The benefits are immediate:

  • Lower TTFB for gated UI/content
  • Better responsiveness for geo-targeted releases
  • Lower origin load during heavy traffic

But there is a catch: edge nodes are distributed, so updates are never perfectly instantaneous everywhere.

A practical architecture

A production-ready setup for Cloudflare Workers usually looks like this:

  • Authoritative config store: durable source of truth (R2, D1, or external control plane)
  • Edge cache layer: KV for read path
  • Execution runtime: Worker evaluates flags per request
  • Versioning + propagation: every config publish increments version
  • Observability: decision logs and error counters

A good mental model:

  • Writes are centralized and controlled
  • Reads are distributed and fast

Data model

Keep the model boring and explicit.

export type Rule = {
  kind: "percentage" | "attribute" | "country";
  value: number | string | string[];
  op?: "eq" | "in";
};

export type FlagConfig = {
  key: string;
  enabled: boolean;
  rules: Rule[];
  defaultVariant: "control" | "treatment";
  salt: string;
};

export type FlagSnapshot = {
  version: number;
  updatedAt: string;
  flags: Record<string, FlagConfig>;
};

Two operational rules matter:

  • Every publish is immutable by version
  • Worker only evaluates a fully parsed snapshot

Avoid per-flag ad hoc fetches on request path.

Request-time evaluation flow

Each request follows this flow:

  1. Resolve identity key (userId, accountId, or stable anonymous ID)
  2. Load latest snapshot from KV cache (with short TTL)
  3. Evaluate target flag rules in deterministic order
  4. Return decision headers and continue rendering
function stableBucket(identity: string, salt: string): number {
  // deterministic 0..99
  let hash = 2166136261;
  const input = `${identity}:${salt}`;
  for (let i = 0; i < input.length; i++) {
    hash ^= input.charCodeAt(i);
    hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
  }
  return Math.abs(hash) % 100;
}

function isEnabledForUser(flag: FlagConfig, identity: string): boolean {
  if (!flag.enabled) return false;

  for (const rule of flag.rules) {
    if (rule.kind === "percentage") {
      const pct = Number(rule.value);
      return stableBucket(identity, flag.salt) < pct;
    }
  }

  return flag.defaultVariant === "treatment";
}

Determinism is non-negotiable. If a user gets treatment once, they should keep it across requests.

Rollout strategy that avoids incidents

A standard rollout ladder works well:

  • 1% internal users
  • 5% low-risk segment
  • 25% all traffic
  • 50%
  • 100%

The real trick is not the percentages. It is the pause conditions.

Before every step, check:

  • Error rate change (control vs treatment)
  • Latency delta (p95/p99)
  • Conversion or critical funnel metric
  • Infrastructure pressure (CPU, egress, downstream saturation)

If any guardrail breaks, stop and roll back immediately.

Consistency tradeoffs and how to handle them

At the edge, consistency is probabilistic over short windows.

Common issues:

  1. Propagation lag after publish
  2. Mixed experience during config transition
  3. Stale local cache at specific POPs

Mitigations:

  • Version snapshots and return x-flag-version header
  • Keep short TTL for edge snapshot cache (for example 15 to 60 seconds)
  • Support emergency bypass for force-off flags
type EvalResult = {
  enabled: boolean;
  reason: "global_off" | "percentage" | "default" | "missing_flag";
  version: number;
};

Adding reason codes seems minor until you need to debug a rollout quickly.

Failure modes you should design for

1) Config fetch failures

Do not fail open by default for risky features.

Safer default:

  • If snapshot unavailable, use last-known-good in memory
  • If no snapshot available at all, return control

2) Corrupt config publish

Require strict schema validation before publishing.

  • Reject malformed rules
  • Reject unknown rule kinds
  • Reject missing salts

3) Identity instability

If identity changes between requests (for example, random anonymous IDs per page), percentage rollout becomes noisy and users jump variants.

Always use a stable key.

4) Hidden coupling

Flags controlling database writes, billing paths, or migrations should never be “UI-only” flags. They need stronger controls and explicit rollback playbooks.

Worker implementation pattern

A simple pattern works well in Cloudflare Workers:

export interface Env {
  FLAGS_KV: KVNamespace;
}

const SNAPSHOT_KEY = "flags:latest";

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const snapshotRaw = await env.FLAGS_KV.get(SNAPSHOT_KEY, "text");
    const snapshot: FlagSnapshot | null = snapshotRaw ? JSON.parse(snapshotRaw) : null;

    const identity = req.headers.get("x-user-id") ?? "anonymous";

    const flag = snapshot?.flags["new_checkout_flow"];
    const enabled = flag ? isEnabledForUser(flag, identity) : false;

    const res = new Response(JSON.stringify({ enabled }), {
      headers: { "content-type": "application/json" },
    });

    if (snapshot) {
      res.headers.set("x-flag-version", String(snapshot.version));
    }

    return res;
  },
};

For high-traffic apps, add in-worker short-lived memory cache to reduce repeated KV reads.

Observability you actually need

Track these metrics from day one:

  • Flag evaluation count by flag key and variant
  • Evaluation failures by reason
  • Snapshot age by POP
  • Rollout step progression events
  • Correlation to product and reliability metrics

Without these, you cannot answer the only question that matters during rollout:

“Did treatment cause this regression, or is this unrelated noise?”

Security and governance

Feature flags can become an unauthorized control plane if unmanaged.

Minimum controls:

  • RBAC on who can publish
  • Audit log for every change
  • Approval flow for high-risk flags
  • Time-bound flags with expiry owner

A practical policy is:

  • Every new flag must have: owner, purpose, expiry date
  • Expired flags get deleted, not ignored

Long-lived dead flags create branching logic debt and hide bugs.

When edge flags are a bad fit

Use edge flags selectively. They are not universal.

Avoid edge evaluation when:

  • Decision needs strong transactional consistency with origin writes
  • Flag depends on sensitive attributes not safe to expose broadly
  • You cannot tolerate short propagation windows

In those cases, keep evaluation in a centralized backend and optimize elsewhere.

Production checklist

Before you roll this into your critical path, verify:

  • Deterministic bucketing with stable identity
  • Snapshot schema validation on publish
  • Versioned snapshots and decision reason codes
  • Last-known-good fallback logic
  • Rollout ladder with explicit pause conditions
  • Metric dashboards for treatment vs control
  • Emergency global off switch tested
  • Expiry policy for flag cleanup

Closing

Feature flags at the edge are not just a performance optimization. They are a release safety system.

Done well, they let you ship faster with smaller blast radius. Done casually, they become a distributed source of confusion.

Design for deterministic decisions, observable rollouts, and fast rollback. If those three are in place, edge flags become one of the highest leverage tools in your DevOps workflow.

More in DevOps

How Terraform Works Internally: HCL Parsing, Dependency Graph Execution, and the Provider Protocol Behind Infrastructure as Code
DevOps ·

How Terraform Works Internally: HCL Parsing, Dependency Graph Execution, and the Provider Protocol Behind Infrastructure as Code

A deep dive into Terraform's internal architecture covering HCL parsing, the expression evaluation engine, DAG-based dependency resolution, the gRPC provider plugin protocol, state mechanics, and the plan/apply lifecycle.

How Docker Works Internally: Namespaces, Cgroups, Union Filesystems, and the OCI Runtime Behind Every Container
DevOps ·

How Docker Works Internally: Namespaces, Cgroups, Union Filesystems, and the OCI Runtime Behind Every Container

A deep dive into what happens when you run docker run: Linux namespaces for process isolation, cgroups v2 for resource enforcement, overlay2 union filesystem for layered images, the OCI runtime spec and how runc spawns containers, content-addressable storage, the containerd shim architecture, and networking with veth pairs.

How Kubernetes Works: Pods, Scheduling, and the Reconciliation Loop
DevOps ·

How Kubernetes Works: Pods, Scheduling, and the Reconciliation Loop

A deep dive into Kubernetes internals covering the control plane architecture, etcd watch mechanics, the scheduler's filter and score phases, controller manager reconciliation loops, kubelet pod assignment, the CRI and CNI plugin models, and production considerations for resource requests, pod disruption budgets, and cluster autoscaler behavior.

How Docker Works: Namespaces, Cgroups, Union Filesystems, and the Container Runtime from Build to Execution
DevOps ·

How Docker Works: Namespaces, Cgroups, Union Filesystems, and the Container Runtime from Build to Execution

A deep dive into Docker internals covering Linux namespace isolation, cgroup resource constraints, OverlayFS copy-on-write layer mechanics, Dockerfile build cache invalidation rules, the containerd and runc OCI runtime stack, and production considerations for image hardening and startup latency.