Skip to content

Cloudflare Workers Feature Flags Quickstart

This guide takes a Cloudflare Worker from an empty handler to its first evaluated feature flag. The @featureflip/edge package keeps a copy of your environment’s flag config next to the Worker and evaluates against it. When a request finds a warm copy, reading a flag makes no network call at all.

  • A Featureflip account with at least one feature flag created
  • A server SDK key from your environment settings
  • A Cloudflare Workers project

Cloudflare Workers is the only runtime this SDK supports.

Terminal window
npm install @featureflip/edge
Terminal window
npx wrangler secret put FEATUREFLIP_SERVER_KEY

Wrangler prompts for the value and binds it to your Worker as env.FEATUREFLIP_SERVER_KEY. For wrangler dev, put the same line in .dev.vars. The server key can read every flag in the environment, so it belongs in a secret and never in code that reaches a browser.

import { createEdgeClient, type EdgeClient } from '@featureflip/edge';
interface Env {
FEATUREFLIP_SERVER_KEY: string;
}
// One client per isolate, reused by every request it serves.
let flags: EdgeClient | undefined;
const VISITOR_COOKIE = 'ff_visitor';
function readCookie(request: Request, name: string): string | undefined {
const header = request.headers.get('Cookie') ?? '';
for (const part of header.split(';')) {
const [key, ...rest] = part.trim().split('=');
if (key === name) return rest.join('=');
}
return undefined;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
flags ??= createEdgeClient({ sdkKey: env.FEATUREFLIP_SERVER_KEY });
// The SDK never makes up a visitor key. This Worker keeps its own in a cookie.
const existing = readCookie(request, VISITOR_COOKIE);
const visitorId = existing ?? crypto.randomUUID();
const snapshot = await flags.load(ctx);
const context = { user_id: visitorId, country: request.cf?.country };
const newCheckout = snapshot.boolVariation('new-checkout', context, false);
const response = new Response(newCheckout ? 'New checkout' : 'Classic checkout');
if (!existing) {
response.headers.append(
'Set-Cookie',
`${VISITOR_COOKIE}=${visitorId}; Path=/; Max-Age=31536000; Secure; HttpOnly; SameSite=Lax`,
);
}
return response;
},
};

The flags variable sits at module scope, yet the client gets built inside fetch. Workers forbid I/O at module scope and your key only shows up on env, so flags ??= createEdgeClient(...) waits for the first request, and from then on the isolate reuses that client. Building it is free. createEdgeClient makes no request of its own.

Call load(ctx) once per request. It resolves to a snapshot of the flag config, and the variation calls you make on that snapshot are synchronous. Hand it ctx. The SDK passes its background refresh and its event flush to waitUntil, and both finish after your response is on its way.

Your cookie supplies the user_id, which is the key a percentage rollout buckets on. Keep the cookie and a visitor stays in one bucket on every request. Clear it and they come back as a new UUID, maybe in the other bucket. Signed-in users are easier: pass the account ID.

The last argument to boolVariation is the default. It comes back for a flag that doesn’t exist, a served value that isn’t a boolean, and any request made before a config has loaded. Other value types have their own accessors:

const copy = snapshot.stringVariation('checkout-copy', context, 'control');
const limit = snapshot.numberVariation('cart-limit', context, 20);
const banner = snapshot.jsonVariation('promo-banner', context, { show: false });

Pages Functions run on Workers too. Their context argument has a waitUntil method, so it goes straight into load:

functions/index.ts
import { createEdgeClient, type EdgeClient } from '@featureflip/edge';
interface Env {
FEATUREFLIP_SERVER_KEY: string;
}
let flags: EdgeClient | undefined;
export const onRequest: PagesFunction<Env> = async (context) => {
flags ??= createEdgeClient({ sdkKey: context.env.FEATUREFLIP_SERVER_KEY });
const snapshot = await flags.load(context);
const visitorId = context.request.headers.get('Cookie')?.match(/ff_visitor=([^;]+)/)?.[1];
const enabled = snapshot.boolVariation('new-checkout', { user_id: visitorId }, false);
return new Response(enabled ? 'New checkout' : 'Classic checkout');
};

When the cookie is missing, set it the same way the Worker example does. Until a visitor has one, every percentage rollout skips them and they get the next matching rule or the default variation.

Every flag returns its default

Search your Worker logs for [featureflip-edge], and for [featureflip], the prefix on the few messages that come from the evaluation core (rejected or dropped events, an ignored wrappers token). SDK key rejected (401) means a wrong or revoked key, and a client key fails the same way because this SDK needs a server key. no config within 1000ms is a cold isolate whose first fetch ran past loadTimeoutMs. That request got defaults while the fetch finished in the background, and the next one should have flags. snapshot.ready is false whenever no config is loaded.

A flag change hasn’t shown up yet

Changes arrive within ttlSeconds (60 seconds by default), so give it a minute. If it still hasn’t moved, compare the key’s case with the dashboard (new-checkout and New-Checkout are two flags) and check that the flag is on in the environment your SDK key belongs to.

A warning about running without waitUntil

Your code called load() with no argument. Pass the request’s ctx, or context in a Pages Function.