| name | feature-flag-rollout-strategist |
| description | Use when introducing a feature flag system, choosing between release flags / experiment flags / operational flags / kill switches / permission flags, designing percentage rollouts, building kill-switch + circuit-breaker patterns, structuring evaluation contexts (user IDs, plan tier, region), or building flag-cleanup discipline. Triggers: LaunchDarkly / GrowthBook / Unleash / Flagsmith / OpenFeature, percentage ramp, sticky bucketing, kill switch wired to alerting, "we still have flags from 2 years ago", flag debt, OpenFeature provider, evaluation context, default value on provider failure. NOT for A/B test statistical analysis (separate skill), traffic-split routing at the edge, build-time bundler defines, or environment-based config. |
| category | Backend & Infrastructure |
| allowed-tools | Read,Grep,Glob,Edit,Write,Bash |
| tags | ["feature-flags","feature-management","launchdarkly","openfeature","rollouts","kill-switch"] |
Feature Flag Rollout Strategist
Three choices that decide whether a feature-flag system helps you or owns you: the right flag type, a hard cleanup discipline, and fail-safe default values when the flag service is down. Get those three and the rest is configuration.
The 2026 industry default is OpenFeature as the vendor-neutral SDK API (CNCF, vendor-implementations from LaunchDarkly, GrowthBook, Flagsmith, Flipt, Unleash all publish OpenFeature providers) — code against the spec, swap providers if you change vendors. (OpenFeature spec)
LaunchDarkly's distinction is the cleanest: release flag = short-lived, kill-switch = also short-lived but during release, circuit breaker = long-lived, alert-driven. (LaunchDarkly — Operational Flag Best Practices)
Jump to your fire:
When to use
- Adding feature flags to a service for the first time.
- Existing flag usage has produced flag debt; team wants a cleanup pattern.
- Rolling out a risky migration (DB, auth) where percentage ramp + instant rollback is required.
- Wiring kill switches into the alerting system.
- Choosing between OpenFeature + a backend, or going direct with one vendor.
Core capabilities
Flag types
Different flag types have different lifetimes, owners, and risk profiles. Mixing them is how the catalog rots. Unleash's classification (echoed by the OpenFeature ecosystem) is widely useful: (Unleash docs)
| Type | Purpose | Expected lifetime | Owner | Cleanup trigger |
|---|
| Release | Hide an in-progress feature; ramp once it's ready | ~30–60 days | Feature team | Feature is at 100%, stable |
| Experiment | A/B test, multivariate | One experiment cycle | Product / data | Experiment concludes |
| Operational | Long-lived control of a behavior (rate limit, cache TTL) | Permanent | SRE / platform | Replaced or no longer applicable |
| Kill switch | Short-lived, deploy-tied; flip off if something breaks | One release cycle | Feature team | Same as the release flag |
| Circuit breaker | Long-lived, alert-driven; auto-opens to degrade gracefully | Permanent | SRE | Feature retired or replaced |
| Permission | Entitle access by plan, role, region | Until product change | Product / billing | Plan / region changes |
LaunchDarkly's working terminology: "A kill switch typically refers to a short-term flag used during a release to turn a feature off if something goes wrong. A circuit breaker is considered a permanent flag and is activated based on an event." (ld-operational)
Practical rule: name the type into the flag, e.g. release-checkout-v2-temp, ops-search-cache-perm, kill-payments-v3-temp. LaunchDarkly's recommended naming bakes type + creation date into the flag key for exactly this reason. (ld-operational)
Ramp pattern
The default safe ramp:
1% → 5% → 25% → 50% → 100%
↑ pause for at least one full traffic cycle (≥ 1 hour for hot APIs)
At each step, watch:
- Error rate on the gated path (and adjacent paths — feature flags have side effects).
- Latency p99 on the gated path.
- Business metric the feature exists to move (signup conversion, checkout completion).
If any gets worse, roll back to the previous step or off entirely. Don't push through.
import { OpenFeature } from '@openfeature/server-sdk';
import { LaunchDarklyProvider } from '@openfeature/launchdarkly-server-provider';
await OpenFeature.setProviderAndWait(new LaunchDarklyProvider(process.env.LD_SDK_KEY!));
const client = OpenFeature.getClient();
const enabled = await client.getBooleanValue(
'release-checkout-v2-temp',
false,
{ targetingKey: user.id, plan: user.plan, region: user.region }
);
The targetingKey is the bucket-stable hash input. Always pass a stable user/account ID, not a session/request ID, or users will see the feature flicker.
Kill switches and circuit breakers
Kill switches save outages. Two patterns:
Manual kill switch (the human flips):
const allowPayments = await client.getBooleanValue('kill-payments-v3-temp', true, ctx);
if (!allowPayments) {
return c.json({ error: 'payments-temporarily-unavailable' }, 503);
}
Default true — if the flag service is unreachable, the feature stays available. Flip to false in the LaunchDarkly UI to disable.
Automatic circuit breaker (alerts flip):
LaunchDarkly's recommended pattern: "a monitoring tool generates an alert when orders fail to complete. The alert toggles a flag that sets the site to 'read-only.'" (ld-operational)
PagerDuty / Grafana alert → webhook → Flag API: set "ops-checkout-readonly-perm" = true → app degrades
The flag remains false by default; an alert flipping it to true puts the system into a degraded mode while humans investigate. Pair with grafana-dashboard-builder for the alert side.
Cleanup discipline
Stale flags are the cost. Without enforcement, every flag system eventually has hundreds of flags nobody knows what to do with.
The recommended pattern from LaunchDarkly and Unleash, paraphrased: (ld-operational, Unleash)
- Every flag has a maintainer assigned at creation time. Documented in flag metadata.
- Every release/experiment flag has a TTL at creation. Default 30 days, max 90.
- Cleanup is scheduled: monthly or quarterly review; flags past their TTL are owners' responsibility to remove or extend (with justification).
- Auto-cleanup tools are run quarterly: dead-flag finders (
flagshark, unrevoked, grit.io flag transformations) identify flags whose code has been at 100% for ≥ 30 days.
- Code-side cleanup: when removing a flag, both the flag definition AND the conditional code must be removed. Half-removals leave dead branches.
git log --since='90 days ago' --format='%H' -- src/ | xargs git diff --name-only | sort -u
The 30-day sunset policy is widely cited as the practical baseline. (FlagShark — Best Feature Flag Cleanup Tools)
Default values and provider failure
The single most-overlooked pattern. The flag SDK MUST handle:
- Provider unreachable (network down).
- Provider slow (timeout).
- Provider returns invalid value (vendor outage).
const safeMode = await client.getBooleanValue(
'ops-feature-x-perm',
true,
ctx
);
The choice between true and false as default is the most important design decision per flag:
| Default | Use when |
|---|
true (feature on) | The feature is the safe path; turning it off only when explicitly needed |
false (feature off) | The feature is the risky path; only enable when explicitly approved |
Kill switches default true (feature available). Release flags default false (feature hidden). Don't get this backwards.
Evaluation context
The dimensions you target on. Common shapes:
const ctx = {
targetingKey: user.id,
email: user.email,
plan: user.plan,
region: user.region,
signupCohort: user.signupMonth,
internal: user.email.endsWith('@yourorg.com'),
};
Pre-launch the feature to internal users (internal: true rule) to dogfood, then ramp to a 1% public segment, then up. Same flag, different rules.
Vendor selection
| Need | Reach for |
|---|
| Best-in-class managed, large team | LaunchDarkly |
| OSS self-hosted, GrowthBook is the leader | GrowthBook |
| OSS lightweight, Unleash | Unleash |
| Cheap managed, simple | Flagsmith / Flipt / PostHog |
| Edge/Workers-friendly | Cloudflare Worker bindings, ConfigCat, Statsig |
| Multi-vendor / future-proof | OpenFeature SDK + any provider |
Code against the OpenFeature API regardless of provider. The cost of switching vendors is then the provider config, not the call sites. (openfeature-spec)
const enabled = await client.getBooleanValue('release-checkout-v2-temp', false, ctx);
Test harness
import { InMemoryProvider } from '@openfeature/in-memory-provider';
await OpenFeature.setProviderAndWait(new InMemoryProvider({
'release-checkout-v2-temp': { variants: { on: true, off: false }, defaultVariant: 'on', disabled: false },
}));
Two test paths per flag (on, off). When the flag is removed, the second path is removed too.
Targeting expressions: keep them simple
plan IN ['pro','enterprise']
AND region IN ['us-east-1']
AND createdAt < '2026-01-01'
Beyond ~3 conditions, targeting becomes a black box that nobody understands six months later. If a rule is complex, encode the cohort in the evaluation context (signupCohort, treatmentGroup) rather than computing it in the flag service.
Anti-patterns
Permanent flag with no maintainer
Symptom: A flag from 2023 still in code; nobody knows what it does. Removing it scares everyone.
Diagnosis: No owner attached at creation; no review cadence.
Fix: Maintainer field required at creation; quarterly cleanup review with the owner; if no owner, default-off and remove.
Default value that turns into the outage
Symptom: Flag service has a 30-second blip; the entire feature goes dark for 30s because the default was false.
Diagnosis: Default was set as if the flag were a release flag, but the feature was already at 100%.
Fix: When ramping past 100%, flip the default to true so a provider blip doesn't cause an outage. Better: remove the flag.
Session-ID bucketing
Symptom: Users see the feature one request, miss it the next. Bug reports about flicker.
Diagnosis: targetingKey was session.id instead of user.id.
Fix: Always bucket on a stable identity. For unauthenticated traffic, bucket on a stable cookie that survives reload, not the request.
Big-bang flip from 0% to 100%
Symptom: A bug at scale that wasn't visible at 1% takes down production.
Diagnosis: Skipped the ramp.
Fix: Use the standard ramp (1% → 5% → 25% → 50% → 100%) with at least one traffic cycle between steps.
Code at 100% but flag still in place "just in case"
Symptom: The dead branch (if (!flag) { /* old code */ }) rots. Six months later it's incompatible with the codebase but nobody noticed.
Diagnosis: Flag never cleaned up after ramp completion.
Fix: Cleanup is a release task: when a flag hits 100% stable for 30 days, delete the flag AND the dead branch in the same PR.
Targeting rule as business logic
Symptom: "Users on the pro plan in EU after May 1 with X feature get Y treatment" — encoded in flag rules across 12 flags.
Diagnosis: Flag service is being used as a rules engine.
Fix: Compute the cohort in code; pass cohort: 'eu-pro-may' in evaluation context; flag rule reads only that key. One source of truth.
Flag service treated as cache
Symptom: Feature behavior depends on flag value being read on every request; flag service latency dominates p99.
Diagnosis: No SDK-side caching, no streaming updates.
Fix: All major SDKs cache and stream updates; if you've configured around the cache, undo it.
No kill- prefix on kill switches
Symptom: During incident: "wait, which flag is the kill switch?"
Diagnosis: Naming doesn't distinguish.
Fix: Adopt LaunchDarkly's naming convention: release-, kill-, ops-, experiment-, permission- prefix. Type encoded in the name.
Quality gates
NOT for
- A/B test statistical analysis — different problem (significance, p-values, sample sizes). Pair with a stats skill.
- Edge / traffic-split routing (Cloudflare splitTesting, Vercel rewrites) — different layer; use feature flags only for the application-side branch.
- Build-time bundler defines (
process.env.FEATURE_X) — different lifecycle (rebuild required to flip).
- Environment-based config (DB URLs, secrets) — different concern; don't use a flag service.
- DB schema migrations gated by flags — overlaps; the flag gates code behavior. Schema lifecycle is →
zero-downtime-database-migration.
- Authorization decisions in production — flags can entitle access (permission flags) but security policy is a separate boundary; don't put critical authz in feature flags only.
Sources