Loading introduction
Loading stats
Loading about
Loading posts
Loading article
Skip to content
Md Nayeem Hossain
Explorer
home.tsx
feature-flags-done-right.md
feature-flags-done-right.md
blog.json

// DevOps

Feature Flags Done Right: Ship Safely Without Long-Lived Branches

Separate deploy from release with feature flags, roll out progressively, and avoid the flag debt that turns a superpower into a trap.

Md Nayeem HossainOct 13, 20253 min read
🚀

Feature Flags Done Right: Ship Safely Without Long-Lived Branches#

The first time a team adopts feature flags, it feels like a superpower. Merge unfinished work to main, deploy anytime, turn features on for internal users first, kill a broken feature in seconds without a redeploy.

The second year, it often feels like a trap: hundreds of flags nobody remembers, code paths that are impossible to test, and an outage caused by flipping a flag that was supposed to be dead.

Here is how to get the first experience without the second.

Separate deploy from release#

The core idea is simple. Deploying puts code on servers. Releasing exposes behavior to users. Flags let you do them independently:

ts
1if (await flags.isEnabled("new-checkout", { userId, plan })) {2  return renderNewCheckout(cart);3}4return renderLegacyCheckout(cart);

This lets the team merge small changes to main continuously instead of maintaining a feature branch for weeks and fighting a giant merge at the end.

Know which kind of flag you are creating#

Different flags have very different lifetimes, and mixing them up causes most flag debt:

  • Release flags hide incomplete or risky work. They should live days or weeks, then be removed.
  • Experiment flags split traffic for A/B tests. They live until the experiment reaches a decision.
  • Ops flags (kill switches) disable expensive or fragile features during incidents. They can be long-lived, but should be few and well documented.
  • Permission flags gate features by plan or customer. These are really product configuration and often belong in your entitlements system rather than your flag tool.

Tag every flag with its type and an owner when you create it.

Roll out progressively#

A good release flag goes through stages:

  1. Internal team only.
  2. 1% of users, watching error rates and latency.
  3. 10%, 50%, then 100% over hours or days.
  4. Remove the flag and the old code path.

Percentage rollouts must be sticky: hash a stable identifier so the same user always gets the same variant.

ts
1function inRollout(flag: string, userId: string, percent: number) {2  const hash = murmurhash(`${flag}:${userId}`) % 100;3  return hash < percent;4}

Including the flag name in the hash keeps different rollouts independent, so the same 1% of users are not the guinea pigs for every feature.

Evaluate flags close to the code, safely#

Flag checks run on hot paths, so they must be fast and resilient:

  • Cache flag rules in memory and refresh them in the background. Never make a network call per check.
  • Define a safe default for every flag. If the flag service is unreachable, the code must still behave correctly — usually by falling back to the old path.
  • Evaluate a flag once per request and pass the result down, so a single request never sees both variants.

Testing with flags#

Every flag doubles the number of possible code paths. You cannot test every combination, but you can test the ones that matter:

  • Unit test both branches of each active flag.
  • Run end-to-end tests against the configuration production actually uses.
  • Make flags overridable in tests, so a test states its assumptions explicitly.

Flag debt is real debt#

The biggest risk with flags is never removing them. Old flags make code harder to read, and a forgotten flag can be flipped by accident. I have seen a stale flag re-enable a payment path that had been deprecated for a year.

Habits that prevent this:

  • Create the removal ticket at the same time as the flag.
  • Set an expiry date, and have the flag system warn when it passes.
  • Report on flags that have been at 100% (or 0%) for more than two weeks — those are ready to delete.
  • Treat removing a flag as part of finishing the feature, not as optional cleanup.

Audit and access#

Flipping a flag is a production change. It deserves the same controls as a deploy: an audit log of who changed what and when, permissions for production flags, and ideally a change note. When an incident starts, "what changed in the last hour?" must include flag changes.

Conclusion#

Feature flags are one of the best tools for shipping safely, as long as you treat them as temporary by default. Separate deploy from release, roll out progressively with sticky hashing, define safe defaults, and delete flags as soon as they have done their job.

#Feature Flags#Continuous Delivery#Release Engineering
OlderPostgreSQL Indexing Deep Dive: B-tree, GIN and Partial IndexesNewer Observability Beyond Logs: Traces, Metrics and SLOs

Outline

  • Feature Flags Done Right: Ship Safely Without Long-Lived Branches
  • Separate deploy from release
  • Know which kind of flag you are creating
  • Roll out progressively
  • Evaluate flags close to the code, safely
  • Testing with flags
  • Flag debt is real debt
  • Audit and access
  • Conclusion