← Recipes

阅读中文版本

Release Behind a Flag and Remove It on Schedule

Ship dark, enable for a small percentage, watch metrics, and delete the flag within a fixed window so branches do not accumulate.

~25 minreleasefeature-flagsoperations

Task

Decouple deploy from release and control exposure without leaving permanent conditional branches in the code.

When to use this

A feature ships disabled behind a flag, the flag stays in the codebase for two years, and every new engineer has to reason about a branch nobody remembers.

Prerequisites

  • A flag provider or a config-backed flag table
  • Metrics emitted with the flag variant as a dimension
  • A tracking issue or checklist for flag removal

Steps

1. Declare the flag with an owner and an expiry date in code review.

const flags = {
  'checkout.new-summary': {
    owner: 'checkout-team',
    expires: '2026-12-01',
    defaultValue: false,
  },
};

Expected output: The flag carries an owner and an expiry that reviewers can check.

2. Enable for internal users first, then a small percentage of traffic.

await flags.set('checkout.new-summary', {
  percentage: 5,
  audience: 'internal',
});

Expected output: Internal traffic exercises the new path while error rate and latency are compared by variant.

3. Promote or roll back by changing exposure, without a redeploy.

curl -fsS -X PATCH https://flags.example.com/api/flags/checkout.new-summary -H 'Content-Type: application/json' -d '{"percentage":100}'

Expected output: Exposure changes in seconds while the deployed image stays exactly the same.

4. Remove the flag and the dead branch after full rollout, before the expiry date.

grep -rn 'checkout.new-summary' src/ | wc -l

Expected output: The count reaches zero, the disabled path is deleted, and the tracking issue closes.

Troubleshooting

Problem

Flag evaluation adds latency or fails

Solution

Cache flag state and always fall back to the safe default. Never block a request on the flag provider.

Problem

Metrics cannot be compared by variant

Solution

Emit the flag variant as a metric dimension; without it a regression is invisible and the flag proves nothing.

Limitations

  • Percentage rollouts are not stable per user unless the evaluation uses a stable identifier.
  • Flags become permanent runtime branching when removal is not tracked and enforced.

Separate deploy from release

A flag decouples the moment code reaches production from the moment users see its behavior. That separation is the point: deploying behind a disabled flag lets the team verify that the code starts, serves traffic, and writes metrics before anyone depends on it. It also makes rollback a configuration change instead of a redeploy. The benefit disappears if the flag is never removed, because the codebase accumulates branches that reference features nobody can find in the product.

Roll out in steps with evidence

Start with internal users, then a small percentage. Compare error rate, latency, and business signals between variants at each step, and only widen exposure when the comparison is stable. Keep the default value safe, and cache the evaluation result so a provider outage degrades to the old behavior instead of an error. If the provider is unreachable at startup, the service should still start.

await flags.set('checkout.new-summary', { percentage: 5, audience: 'internal' });

Make removal part of the release

Every flag needs an owner and an expiry date, and both belong in the pull request that introduces it. After full rollout, delete the flag, the conditional branch, and any configuration entries in one change; leaving the flag in place with a hardcoded value is how dead code survives. Track the removal as an issue with the same priority as the feature itself. Review the flag inventory monthly, because the cost of a stale flag is paid by every future change to that file.