Feature flags and their lifecycle
Deploy ships code switched off; a flag edit releases it or takes it back; each kind has a lifetime, and a leftover flag is debt.
Predict first
Before you read: predict
The shop deploys build 7.3 of its checkout service to every server at 09:00. The build contains a new express checkout:
At 09:00 the flag system holds:
{ "express_checkout": { "state": "ENABLED", "variants": { "on": true, "off": false }, "defaultVariant": "off",
targeting": { "if": [ { "in": [ { "var": "country" }, ["GB"] ] }, "on", "off" ] } } }
At 09:30 the product owner edits the flag in its dashboard so that the list reads ["GB", "IE"]. Nothing is deployed or restarted. At 09:45 three shoppers check out, from GB, IE and FR.
Who gets express checkout?
const client = OpenFeature.getClient();export async function checkoutPage(shopper) { const express = await client.getBooleanValue('express_checkout', false, { country: shopper.country }); return express ? 'express checkout' : 'standard checkout';}The 30-second version
A feature flag lets one deployed build run old or new code, chosen at runtime from its configuration and the request's context. Deploying ships the code switched off; releasing is a configuration edit, and so is taking it back. Release, experiment, ops and permission flags live from days to years. A flag past its job is debt.
Deploy ships code switched off; a flag edit releases it or takes it back; each kind has a lifetime, and a leftover flag is debt.
How it works
Step 1 of 13
Analogy: A swimming pool's new water flume
The flume is installed and plumbed during the closed weeks, but the gate at its top stays chained until the duty manager unlocks it.
| At the pool | In the code |
|---|---|
| the flume is built and tested, but the top gate stays chained | deployed, not released (step 3) |
| the manager unchains the gate on opening day, and chains it again if a joint leaks; nobody rebuilds the flume | release and take-back by configuration (step 4) |
| in trial week, only members whose card number ends in 0 may ride; then cards ending in 0, 1 or 2 | a bucket from a stable key, widened (step 5) |
| a club-only lane that has run for years | a permission flag (step 6) |
| the lifeguard's pump stop, pressed on any overcrowded afternoon | a long-lived kill switch (step 7) |
| the "opening soon" chain still hanging a year later, and nobody sure whether the flume is meant to be open | flag debt (step 11) |
In real terms: a flag is a decision the running code makes on each evaluation, from configuration that changes without a new build.
Breaks down when: a chained gate is visible to everyone. A flag's state is not: two users can be on different paths at once, and the code still holds both paths.
Contrasting case: a flag read from an environment variable at boot is still a flag, but each change needs a restart or a redeploy. That can suit a release flag, but not a kill switch, which must act at once.
Examples, run for real
4.1 ExamHall: the AI alerts that only a redeploy could stop
EXH-2100: build 409 shipped the proctor console's new AI alerts with no flag, so they went live for every institution at deploy time. A false-positive storm followed, and the way back was to redeploy build 408. MR !2400 wraps them in new_proctor_ai_alerts, evaluated per request with the proctor's institution.
4.1 build 409 deployed · flag off · north-college sees AI alerts: no · south-academy: no
4.1 edit: target north-college · north-college: yes · south-academy: no · build 409
4.1 edit: targeting removed · north-college: no · south-academy: no · build 409 · deploys in all: 1
The build never changes. Enabling the alerts for one institution is one edit (steps 2–4), and so is switching them off for everyone.
4.2 Proof domain, checkout: three flags, eight checkouts
The checkout page reads three independent boolean flags. The script sets every combination in the flag system and renders the page each time.
4.2 flags 3 · states 2^3 = 8 · distinct checkout pages 8 · states an all-off + all-on suite covers: 2 of 8
Each flag doubles the states (step 10). A suite that tests only all-off and all-on covers two of the eight pages that production can serve.
4.3 ExamHall: a 10% rollout by proctor id
new_proctor_ai_alerts uses flagd's fractional rule on the proctor id. The script evaluates it ten times for each of 500 proctors.
4.3 500 proctors x 10 evaluations · proctors who got both answers: 0 · proctors with AI alerts: 53 (10.6%)
The bucket comes from a hash of the proctor id, so nobody flips between versions (step 5). It gives about 10%, not exactly 50 proctors.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 5
one_click_pay is rolled out with flagd's fractional rule, which buckets each shopper by targetingKey, the shopper id. On Monday the flag system holds:
{ "one_click_pay": { "state": "ENABLED", "variants": { "on": true, "off": false }, "defaultVariant": "off",
targeting": { "fractional": [ ["on", 5], ["off", 95] ] } } }
On Tuesday the team edits the rule to [["on", 20], ["off", 80]]; nothing else changes. The 2,000 shoppers u0001 to u2000 each check out once on Monday and once on Tuesday.
How many of Monday's one-click shoppers lose it on Tuesday, and how many shoppers have it on Tuesday?
Question 2 of 5
ExamHall's upload service has this module:
The service starts at 08:00, when the flag system holds:
{ "disable_video_upload": { "state": "ENABLED", "variants": { "on": true, "off": false }, "defaultVariant": "off" } }
At 08:55 video storage is overloaded, and the engineer on duty sets disable_video_upload to on in the flag dashboard. 25 sessions opened before 08:55 and 15 after; each sends one video after 08:55. All 40 videos are stored.
Which one change makes that edit stop all 40, with no restart or deploy, while a video sent before 08:55 is still stored?
const client = OpenFeature.getClient();const uploadsOff = await client.getBooleanValue('disable_video_upload', false); export async function handleUpload(session, video) { if (uploadsOff) return { status: 503 }; return store(session, video);}Question 3 of 5
The checkout computes VAT with:
and the flag system holds:
{ "new_tax_engine": { "state": "ENABLED", "variants": { "on": true, "off": false }, "defaultVariant": "on" } }
new_tax_engine has served on to every shopper for 8 months, and the team retires it. The goal: no basket's VAT changes at any step of the cleanup, and afterwards no code reads new_tax_engine.
Which plan meets both?
const client = OpenFeature.getClient();export async function vat(basket, ctx) { const useNew = await client.getBooleanValue('new_tax_engine', false, ctx); return useNew ? newTax(basket) : oldTax(basket);}Question 4 of 5
On a normal day the box office's seat map shows every fan a live heat map of demand. The flag heatmap_off hides it. This is the flag's whole change log:
2025-11-14 09:02 on all fans set by on-call: seat-db CPU alarm
2025-11-14 09:41 off all fans set by on-call
2026-03-02 10:00 on all fans set by on-call: seat-db CPU alarm
2026-03-02 10:35 off all fans set by on-call
2026-09-19 09:00 on all fans set by on-call: seat-db CPU alarm
2026-09-19 09:52 off all fans set by on-call
No rule on the flag has ever named a fan, a group or a user attribute.
Which kind of flag is heatmap_off?
Question 5 of 5
The admin console's flag inventory lists four flags:
grading_ui_v2wraps the rebuilt grading screen, and has servedonto every institution for 9 months;pro_plan_analyticsshows the analytics tab only to institutions on the paid plan, and was added 3 years ago;pause_bulk_exportlets the engineer on duty stop bulk result exports while the database is overloaded; added 2 years ago, it is used on each results day;results_layout_bsends half the proctors to a new results layout; it has run for 3 weeks, and its result is not yet significant.
Which flag is debt today?
Go deeper
Pro tips
- Give every flag an owner and an expiry date in its metadata, and make CI fail while code still reads a flag past its expiry.
When: creating any flag (step 11). Verify:
flag-expiry: FAIL invoice_pdf_v2 (owner billing-team, expired 2026-02-01) still read in src/billing/invoice.ts, exit 1; the flag that has not expired passes. Status: RAN.
Source
openfeature.dev did not answer from this machine that day (connect timeout), so the OpenFeature specification and the flagd docs were read from their source files in the CNCF open-feature GitHub repositories. Vendor and SDK behaviour can change: what the scripts show is SDK 1.23.0 and flagd-core 4.0.1. Valid as of 2026-10-01.
- Pete Hodgson, Feature Toggles (aka Feature Flags), martinfowler.com, 9 October 2017: https://martinfowler.com/articles/feature-toggles.html (
hodgson.txt). The curriculum row's first source. Quoted: "Release Toggles are transitionary by nature. They should generally not stick around much longer than a week or two" and "it's not uncommon for systems to have a small number of long-lived “Kill Switches”". - OpenFeature specification, Flag Evaluation API: https://github.com/open-feature/spec/blob/main/specification/sections/01-flag-evaluation.md (
openfeature-flag-evaluation.txt). The curriculum row's second source. Quoted: "Flag evaluation calls must always return thedefault valuein the event of abnormal execution." - OpenFeature specification, Glossary: https://github.com/open-feature/spec/blob/main/specification/glossary.md (
openfeature-glossary.txt). Quoted: a server SDK "typically uses the [dynamic-context paradigm]". Supports step 13. - OpenFeature specification, Evaluation Context: https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md (
openfeature-evaluation-context.txt). Quoted: an evaluation context has "an optionaltargeting keyfield of type string, identifying the subject of the flag evaluation." Supports steps 2 and 5. - flagd docs, Fractional operation: https://github.com/open-feature/flagd/blob/main/docs/reference/custom-operations/fractional-operation.md (
flagd-fractional.txt). Quoted: "Assignment is deterministic (sticky) based on the expression supplied as the first parameter".
Related lessons
Checked against the tools and docs as of 2026-10-01.