Feature flag best practices

Give every change its own short-lived flag, make the off path the safe one, and evaluate flags in your app so a check never waits on the network. Roll out in steps (your team, chosen users, a sticky percentage, everyone), test both states, record who changed what and why, and delete the flag once the feature is fully on. When AI agents write the code, add scoped tokens, a note on every change and a kill switch the agent can always use.

Updated

The rules, in short

  1. One flag per change, named for what it does.
  2. Make off the safe path.
  3. Evaluate flags in your app, not over the network.
  4. Roll out in steps, with sticky bucketing.
  5. Test the code with the flag on and off.
  6. Give every flag an owner and a one-line summary.
  7. Keep release flags and operational flags apart.
  8. Write down why, on every change.
  9. Limit who and what can change production.
  10. Keep a kill switch that always works.
  11. Remove flags once they're fully on.
  12. Flags are on/off switches, not a config store.

The rest of this page explains each one and shows how Flagger.gg handles it. The practices themselves apply whatever flag tool you use.

1. One flag per change, named for what it does

A flag that gates one change can be turned off without taking anything else with it. A flag shared by three features can't. Name it for the behaviour, such as checkout_v2 or pdf_export, not for a ticket number or a person, so whoever sees it in an incident knows what turning it off does.

In Flagger.gg every flag also has a summary, one plain-English line shown in the Release console, and flagger create requires it:

flagger create pdf_export --summary "Export a report as PDF" --owner "reports team" --note "new export button" --json

2. Make off the safe path

Write the check so that false runs the code you already trust. Then every failure mode lands somewhere safe. Flagger.gg's SDKs treat a flag that's unknown, not loaded yet or errored as off. A typo in a key, or a flag deleted from the service, turns the new path off and leaves the old one running.

For a long-lived switch whose on side is the normal behaviour, such as a third-party integration, decide what should happen when the app starts with no ruleset at all. Flagger.gg's SDKs start from a disk cache of the last ruleset they saw, which covers most restarts, but a fresh machine with no cache and no network answers off.

3. Evaluate flags in your app, not over the network

A flag check runs on hot paths, often many times per request, so it shouldn't make a network call. Good SDKs keep the rules in memory and update them in the background. Flagger.gg's Go and Node SDKs fetch the ruleset, follow a stream that pushes changes within a second, poll every 5 seconds if the stream drops, and keep a last-known-good copy on disk for outages. Checks never call the network, and your end users' data is never sent to Flagger.gg. See local evaluation.

4. Roll out in steps, with sticky bucketing

Turn a feature on for your own team first, then for a few named users, then for a small percentage, then for everyone. Flagger.gg's five states follow that order: off, admins, targets, percent and on, and the rules are kept when the state changes.

Percentages must be sticky, so a user who has the feature at 10% still has it at 25%. Flagger.gg buckets each user by a hash of the flag key and the user id, so raising the share only ever adds people. See percentage rollouts.

flagger set checkout_v2 admins --note "team first" --json
flagger set checkout_v2 percent --percent 10 --note "admins fine for a day" --json
flagger set checkout_v2 on --note "10% clean for 48 hours" --json

5. Test the code with the flag on and off

Both paths ship to production, so both need tests. Flagger.gg's Node SDK has bothFlagStates, withFlag and setFlagOverride, and the Go SDK has the flaggertest package. See testing both sides of a flag.

await flagger.bothFlagStates("pdf_export", async (on) => {
  const res = await renderReport(report);
  expect(res.hasPdfButton).toBe(on);
});

6. Give every flag an owner and a one-line summary

A flag with no owner is a flag no one removes. Flagger.gg's SDKs register each app's flags at start with their key, summary, description and owner, so the service's list always matches the code. A deploy updates that metadata but never overwrites a state someone set.

7. Keep release flags and operational flags apart

  • Release flags hide unfinished or new work. They're temporary and should be removed once fully on.
  • Operational flags are permanent switches, such as a kill switch for a payment provider or an expensive job. Say so in the summary, so no one cleans them up by mistake.

In Flagger.gg a person can mark a flag protected, for payments or sign-in. Agents can then only kill it or ask a person for any other change.

8. Write down why, on every change

In an incident, the first question is what changed. Flagger.gg's audit log records every change by a person, agent token, admin token, SDK registration or import, with the state before and after and the note. The CLI and MCP tools take a note on every write, and agent tokens must give one. See the audit trail for agent changes.

9. Limit who and what can change production

Give each tool and each agent its own credential, scoped to what it needs. A Flagger.gg agent token covers one project, a list of environments and a set of actions (read, create, target, rollout, kill, request), expires after 90 days and can be revoked. With approvals on, agents in production can roll out up to a cap, 10% by default, and anything wider becomes a request a person approves. See scoped agent tokens and human approval.

10. Keep a kill switch that always works

The one change that must never be blocked is turning a feature off. In Flagger.gg, flagger kill <key>, the flag_kill tool or the console turns a flag off for everyone. Kill works on protected flags, never needs approval, is exempt from agent rate limits, and your apps see it within a second.

11. Remove flags once they're fully on

Every flag left in code doubles the paths through it. Once a release flag has been on for everyone and stable, delete the check and the dead path in a small PR, deploy, and then archive the flag. Flagger.gg shows whether a flag is still registered by code (inCode), so you can see when archiving is safe. See flag cleanup. In development Automated stale-flag cleanup.

12. Flags are on/off switches, not a config store

Flags work best as booleans with rules about who gets them. Flagger.gg's checks return true or false; keep limits, prices, copy and secrets in your config or secrets manager, where they're versioned and reviewed as code.

When AI agents write the code

The same rules apply, with more flags and faster. Have every agent put each change behind its own flag, register it with notReady while it's unfinished, tag its writes with a run id (FLAGGER_RUN or --run), and kill its own flag when a check fails. Flagger.gg gives agents these tools through MCP, hosted at mcp.flagger.gg or flagger mcp on stdio, and the flagger CLI with --json on every command. See feature flags for AI coding agents.

FAQ

What are the best practices for feature flags?

One flag per change, a safe off path, local evaluation, staged rollouts with sticky percentages, tests of both states, an owner per flag, a note on every change, scoped access to production, a kill switch that always works, and removing flags once they're fully on.

How many feature flags is too many?

There's no fixed number. The problem is flags that are fully on and still in code. Keep release flags short-lived and remove them once the feature is stable; a flag count that only grows means cleanup isn't happening.

Should a feature flag default to on or off?

Write the check so that off runs the code you already trust. Flagger.gg treats unknown, not loaded and errored flags as off for that reason.

How long should a feature flag live?

A release flag should live until the feature is on for everyone and stable, then be removed. Operational flags, such as kill switches, are meant to stay and should say so in their summary.

Should flag checks call the flag service on every request?

No. Evaluate in your app from rules held in memory, so a check is fast and still answers when the service is unreachable. Flagger.gg's Go and Node SDKs work this way.

Ship your next change behind a flag.

Sign up, add an SDK key to your app and give your agent a token.

Sign up