skills/feature-flag-rollout-strategist/SKILL.md
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.
npx skillsauth add curiositech/windags-skills feature-flag-rollout-strategistInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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:
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)
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:
If any gets worse, roll back to the previous step or off entirely. Don't push through.
// OpenFeature client (vendor-neutral) sketch.
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();
// Sticky bucketing: same user always gets the same answer for a given ramp.
const enabled = await client.getBooleanValue(
'release-checkout-v2-temp',
/* default */ 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 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.
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)
flagshark, unrevoked, grit.io flag transformations) identify flags whose code has been at 100% for ≥ 30 days.# Example: grep for flags whose code is conditional and grep their last-modified date.
git log --since='90 days ago' --format='%H' -- src/ | xargs git diff --name-only | sort -u
# Flags whose code is at 100% (always-on or always-off branch dead) → remove.
The 30-day sunset policy is widely cited as the practical baseline. (FlagShark — Best Feature Flag Cleanup Tools)
The single most-overlooked pattern. The flag SDK MUST handle:
// Always pass a default. Never throw on provider failure — degrade.
const safeMode = await client.getBooleanValue(
'ops-feature-x-perm',
true, // Default: feature is ON. Choose so that provider-down ≠ outage.
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.
The dimensions you target on. Common shapes:
const ctx = {
targetingKey: user.id, // bucket-stable hash input — REQUIRED
email: user.email, // for individual targeting in the UI
plan: user.plan, // 'free' | 'pro' | 'enterprise'
region: user.region, // 'us-east-1' | 'eu-west-1'
signupCohort: user.signupMonth, // for cohort-based ramps
internal: user.email.endsWith('@yourorg.com'), // dogfooding before public ramp
};
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.
| 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)
// Same call site works with LaunchDarkly, GrowthBook, Flagsmith, Flipt, in-memory test provider, etc.
const enabled = await client.getBooleanValue('release-checkout-v2-temp', false, ctx);
// In tests, use the in-memory provider; assert on flag state.
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.
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.
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.
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.
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.
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.
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.
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.
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.
kill- prefix on kill switchesSymptom: 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.
on and off test paths in the suite, using InMemoryProvider or equivalent.release-, kill-, ops-, experiment-, permission-); a maintainer; a creation date; a target removal date (release/experiment) or "permanent" annotation.structured-logging-design, grafana-dashboard-builder); alerting is wired into circuit-breaker flags via webhook.user.id (or stable cookie for unauthenticated), not session/request ID.feature_flag.<key>=<value> for every evaluated flag (see opentelemetry-instrumentation).process.env.FEATURE_X) — different lifecycle (rebuild required to flip).zero-downtime-database-migration.data-ai
license: Apache-2.0 NOT for unrelated tasks outside this domain.
development
Use when designing caching strategies (cache-aside, write-through, write-behind), implementing distributed locks, building rate limiters, leaderboards, real-time streams (XADD/consumer groups), pub/sub, or tuning eviction policies. Triggers: thundering-herd on cache miss, dogpile on key expiry, Redlock vs SET-NX-PX choice, sliding-window rate limiter, hot-key on a single cluster slot, big-key blowup, MULTI/EXEC across slots, KEYS in production. NOT for Redis Cluster operations/admin (different domain), embedded KV (SQLite, leveldb), in-process LRU caches, or Memcached.
tools
Drawing the `'use client'` boundary correctly in React Server Components apps (Next.js App Router, RSC frameworks) — leaf-pushing, slot composition, serialization rules, and environment poisoning prevention. Grounded in react.dev and Next.js 16 docs.
development
Use when designing rate limiting for an API, choosing between token bucket / sliding window / leaky bucket / fixed window, implementing it in Redis, deciding edge (Cloudflare/Upstash) vs origin enforcement, sizing per-user vs per-IP vs per-endpoint quotas, returning the right 429 response with Retry-After, or fixing the boundary-burst bug in fixed-window limiters. Triggers: 429 too many requests, INCR + EXPIRE, ZADD + ZREMRANGEBYSCORE + ZCARD, X-RateLimit-Remaining header, Cloudflare WAF rate limiting rules, Upstash @upstash/ratelimit, leaky bucket shaping vs policing, distributed rate limiter consistency. NOT for DDoS mitigation specifically (different scale), CAPTCHA / bot management, full WAF design, or per-user quota billing.