Deployment & Delivery

Feature Flags

Kill switches, percentage rollout, and user segment targeting enable deployment decoupled from release. LaunchDarkly, Unleash, and Flagsmith implementations, plus managing flag debt over time.

⏱ 10 min read

What it is

Feature flags (also called feature toggles, feature switches, or feature gates) are conditional checks in code that control whether a feature is active for a given context. Instead of branching in version control, teams branch in code — the feature code ships to production but behind a flag, and the flag is toggled to expose the feature progressively or immediately. The essential types of feature flags: Release toggles gate incomplete features for trunk-based development — deployed but not yet enabled; Experiment toggles enable A/B testing by routing user segments to different code paths; Ops toggles are kill switches for risky or performance-sensitive code paths that can be disabled in production without deployment; Permission toggles control feature access by user role or subscription tier.

A feature flag evaluation context includes: the flag key (e.g., new-checkout-flow), the user/entity context (user ID, email, account tier, geography), and optionally the environment (dev/staging/production). The flag management system evaluates rules against the context to return a boolean or multivariate value. Targeting rules can implement: boolean on/off, percentage rollout (10% of users by user ID hash), user whitelist (named users or user segments), attribute-based targeting (users in EU, accounts on Pro tier), and combinations thereof.

The leading flag management platforms are LaunchDarkly (SaaS, market leader, extensive SDK ecosystem, experimentation), Unleash (open source, self-hosted option, simpler feature set), and Flagsmith (open source with hosted option, also supports remote config). Self-hosted solutions using Redis + a thin SDK are viable for teams wanting to avoid SaaS dependency. The critical requirement for any flag system is low-latency evaluation (flags evaluated per-request must not add latency), typically achieved by streaming flag configuration to in-process caches with millisecond evaluation time.

Why it exists

Feature flags solve the coupling between code deployment and feature availability. Without flags, releasing a feature requires: all the code to be complete, tested, and ready; a deployment to production; and accepting that all users immediately see the new feature. This forces large, risky batched releases. Feature flags decompose this into: deploy code continuously (the feature is dark), enable the feature incrementally (percentage rollout), and observe outcomes before full rollout. Combined with trunk-based development, flags enable teams to deploy many times per day while each individual feature rolls out on its own schedule with independent risk control.

When to use

  • Trunk-based development where incomplete features need to be in the main branch but not yet user-visible.
  • Progressive rollout — enabling features for 1% → 10% → 100% of users over days or weeks with monitoring at each stage.
  • Kill switches for high-risk integrations (third-party payment processors, ML models) that may need to be disabled instantly without deployment.
  • Beta access and graduated rollout to specific user segments before general availability.

When not to use

  • Long-lived configuration — feature flags that stay enabled/disabled for years are better modelled as runtime configuration.
  • Access control that requires strict enforcement — feature flags are not a security boundary; use proper authorization checks for permission enforcement.

Typical architecture

LAUNCHDARKLY SDK PATTERN:
  import ldclient
  from ldclient import Context

  # SDK initialises, streams flag config from LaunchDarkly
  # Evaluations are local (in-process) — no network call per eval
  client = ldclient.get()

  # Simple boolean flag
  def checkout(user_id: str, account_tier: str):
      ctx = Context.builder(user_id)
              .set("accountTier", account_tier)
              .build()

      if client.variation("new-checkout-flow", ctx, False):
          return new_checkout_flow()
      else:
          return legacy_checkout_flow()

  # Multivariate flag (A/B/C test)
  def get_recommendation_algorithm(user_id: str):
      ctx = Context.builder(user_id).build()
      algorithm = client.variation(
          "recommendation-algo",
          ctx,
          "collaborative"  # default value
      )
      # Returns: "collaborative", "content-based", or "hybrid"
      return RecommendationEngine(algorithm)

UNLEASH TARGETING RULES (YAML config):
  feature: new-checkout-flow
  enabled: true
  strategies:
    - name: gradualRolloutUserId
      parameters:
        percentage: "10"     # 10% of users
        groupId: "checkout"
    - name: userWithId
      parameters:
        userIds: "user_abc123,user_def456"  # Beta users

FLAG MANAGEMENT SYSTEM ARCHITECTURE:
  ┌───────────────────────────────────────┐
  │  Flag Management Dashboard            │
  │  (LaunchDarkly / Unleash / Flagsmith) │
  └──────────────┬────────────────────────┘
                 │ Stream flag config (SSE/WebSocket)
  ┌──────────────▼────────────────────────┐
  │  Application Server                   │
  │  ┌────────────────────────────────┐   │
  │  │  In-Process Flag Cache (SDK)   │   │
  │  │  Evaluations: ~0.1ms           │   │
  │  └────────────────────────────────┘   │
  │  Per-request flag evaluation:         │
  │    variation("flag-key", context)     │
  └───────────────────────────────────────┘

PERCENTAGE ROLLOUT (consistent hashing):
  User ID hash → bucket (0-99)
  Flag at 10% → enable if bucket < 10
  Flag at 20% → enable if bucket < 20
  (Same user always in same bucket — consistent experience)

Pros and cons

Pros

  • Decouples deployment from release: code ships on developer schedule, feature enables on product/ops schedule — removes release as a coordination bottleneck.
  • Instant kill switch: risky features can be disabled in seconds from a dashboard without a deployment, reducing MTTR for feature-related incidents from 30 minutes (deployment) to 10 seconds.
  • Targeted rollout: beta user programs, geographic rollouts, and percentage-based gradual exposure are all possible without code changes.

Cons

  • Flag debt: accumulated flags that are never cleaned up increase code complexity exponentially — two flags create four code paths; ten flags create up to 1,024 combinations, making the system difficult to reason about and test.
  • SaaS dependency risk: feature flag platforms are in the critical path for request evaluation; LaunchDarkly or Unleash outages can impact the ability to enable emergency features or respond to incidents.
  • Testing complexity: testing all flag combinations thoroughly is impractical; teams often only test the "flag on" and "flag off" states, missing interactions between flags.

Implementation notes

Every feature flag must have an owner and expiry plan. When a flag is created, record: the flag key, owner team, purpose (release toggle / ops toggle / experiment), expected removal date, and the acceptance condition (fully rolled out to 100% for 30 days without issues → remove). Treat flag cleanup as a first-class engineering task scheduled in the sprint after successful full rollout. Stale flags — where the branch the flag gates is no longer the current code path — are particularly dangerous because removing them requires archaeology to determine which path is now canonical.

For SDK design, always provide a safe default value for every flag evaluation. The default is returned when the SDK cannot reach the flag service, when the flag key doesn't exist, or when an evaluation error occurs. Design defaults to match the pre-flag behavior (usually the old/stable code path), so SDK disconnection degrades to the last known stable behavior rather than enabling new, potentially broken features.

Common failure modes

  • Flag debt accumulation: 200 flags in the codebase after 2 years with no cleanup discipline; engineers afraid to remove flags because they don't know if something depends on them; code is riddled with dead branches and if-else spaghetti.
  • Synchronous flag evaluation: SDK configured to make a network call per evaluation rather than using in-process cache; P99 latency increases by 50ms per request; team adds flags to hot paths without understanding performance model.
  • Flag as security control: Team uses a feature flag to gate a premium feature; flag evaluation is client-side or easily bypassed; paying customers' features accessible without payment by toggling the flag in browser DevTools.

Decision checklist

  • Does every flag have an owner, purpose, and expiry/removal criteria documented?
  • Is flag evaluation in-process (cached) rather than network-call-per-request?
  • Are safe default values defined for all flags (fallback when service unreachable)?
  • Is there a flag lifecycle process enforcing cleanup within a defined period post-rollout?
  • Are flags NOT used as the sole security enforcement mechanism for sensitive features?
  • Is flag state included in request context for observability (tracing, logging)?

Example use cases

  • Dark launch of ML recommendation engine: New recommendation service deployed behind a flag; activated for internal users for 2 weeks to validate quality; then 1% of external users for A/B test measuring click-through rate; metric shows 12% improvement; progressively rolled to 100% over 2 weeks without a dedicated release event.
  • Infrastructure migration kill switch: Team migrates from PostgreSQL to CockroachDB; ops toggle use-cockroachdb defaults off; migration tested at 10% read traffic; when a CockroachDB cluster issue emerges at 3am, on-call engineer disables flag from dashboard in 5 seconds, routing all traffic back to PostgreSQL.
  • Regional compliance requirement: GDPR feature required for EU users before product team has capacity to remove old behavior globally; permission toggle enables new GDPR-compliant flow targeting users where geo=EU, leaving other regions on existing flow until full rollout is planned.
  • Canary Releases — Infrastructure-level percentage routing; flags operate at application layer.
  • CI/CD Pipelines — Trunk-based development enabled by feature flags in pipelines.

Further reading