Continuous delivery and continuous deployment both assume that code merged to the main branch is either always releasable, or automatically released. Feature flags address a specific problem this raises: what if a change is finished and safe to deploy, but not yet ready to actually be turned on for users?

What a feature flag actually is

A feature flag (also called a feature toggle) is a conditional check in code — typically driven by an external configuration value rather than hardcoded — that determines at runtime whether a specific piece of functionality is active. Martin Fowler’s detailed treatment of feature toggles frames the core idea precisely: a flag lets you ship code that’s deployed to production but not yet exposed to users, turning “deploy” and “release” into two genuinely separate events instead of one.

That separation is the whole point. Without feature flags, deploying a change and releasing it to users are the same action — the moment code reaches production, users see it. With a flag, code can sit deployed and dormant, then be switched on independently, without any further deployment at all.

Why decoupling deployment from release matters

This separation solves several distinct problems at once:

  • Trunk-based development on long-running features. Large features can be merged to the main branch incrementally, behind a flag, rather than living on a separate long-lived branch that eventually has to be painfully merged all at once — keeping the small, frequent merges that continuous integration depends on, even for work that isn’t finished yet.
  • Gradual, controlled rollouts. A flag can be turned on for a small percentage of users first, then increased — conceptually similar to the canary releases covered in Blue-Green and Canary Deployments, but operating at the application logic level rather than the infrastructure traffic-routing level, and controllable without a new deployment at all.
  • Instant rollback without a redeploy. If a newly released feature causes a problem, turning its flag back off is immediate — no need to revert code, rebuild, and redeploy, which can be the difference between an incident lasting minutes versus much longer.
  • Testing in production, safely. A flag can expose new functionality only to internal users or a specific test cohort, letting a team validate real-world behavior before any external user sees it.

The real cost: flag debt

Feature flags aren’t free. Every flag is a conditional branch that has to be tested in both states, adding real complexity to the codebase for as long as it exists. Flag debt — old, no-longer-needed flags that were never cleaned up after a feature fully launched or was abandoned — accumulates the same way any other form of technical debt does, and for the same underlying reason: removing a flag once it’s served its purpose is easy to deprioritize once the feature it gated feels “done.” A codebase with years of accumulated dead flags becomes genuinely harder to reason about, since a developer can no longer trust that visible code is actually the code users experience.

The practical discipline this requires is treating flag removal as part of the feature’s actual definition of done, not a separate cleanup task to get to eventually — the same way stale infrastructure discussed in Why Cloud Bills Get Out of Control accumulates precisely because “remove it later” rarely has a forcing function behind it.

Key takeaway

Feature flags decouple deploying code from releasing it to users, which enables incremental development on the main branch, controlled rollouts, instant rollback without a redeploy, and safe production testing. That flexibility has a real ongoing cost in added code complexity, and it only pays off if flags are actively retired once they’ve served their purpose — not left in place indefinitely as untracked technical debt.

This article explains general feature flag concepts; specific tooling and implementation patterns vary. See our disclaimer.