A feature flag is an if statement whose answer you can change without redeploying. It lets you ship new code to production switched off, turn it on for a few users first, and switch it off again in seconds if it misbehaves.
What a feature flag is
At its simplest, a flag is a named on/off value that your code checks before running a new code path. Say Kitty has built dark mode for her Tuna App. Instead of replacing the old theme outright, she wraps the new one in a check:
if (flags.isEnabled("dark-mode", { userId: cat.id })) {
renderDarkTheme();
} else {
renderLightTheme();
}Both paths are in the deployed code. Which one a user sees depends on the flag's current value, and that value lives outside the code: in a config file, a database row or, most often, a dedicated flag service. Change the value and the behaviour changes, with no new build and no new deployment.
The check takes the user as context, because a flag doesn't have to be the same for everyone. It can be on for the team and off for everyone else, or on for 10% of users. The flag service holds those rules, and your code only asks 'is this on for this user?'
Deploying is not releasing
Feature flags separate two things that usually happen together:
- Deploying puts a new version of the code on the servers.
- Releasing lets users see the new behaviour.
Without flags, every deployment is a release and carries the full risk of the change. With a flag, Kitty can deploy dark mode on Tuesday with it switched off. The code is live but hidden. She can release it on Thursday by flipping the switch, or never, if plans change.
That split is what makes small, frequent deployments safe. Half-finished work can be merged into the main branch and shipped behind a flag that is off, instead of sitting in a long-running feature branch that drifts further from main every day. Trunk-based development depends on this habit: Branching Strategies compares it with longer-lived branches, and CI/CD pipelines covers the automated deploys that flags pair with.
Rolling out gradually
The video's rollout starts with 10% of users and grows from there. A typical ramp:
- Internal testers only.
- 1% or 5% of real users.
- 10%, then 25%, then 50%, checking error rates and feedback at each step.
- Everyone.
The flag service also has to decide which 10%. If it picked users at random on every request, the same cat would see dark mode on one page and the light theme on the next. Rollouts have to be sticky: the same user gets the same answer every time.
The usual approach is to hash the user's id together with the flag's name, turn the hash into a number from 0 to 99, and compare it with the rollout percentage:
import { createHash } from "node:crypto";
function bucket(flag: string, userId: string) {
const digest = createHash("sha256")
.update(`${flag}:${userId}`)
.digest();
return digest.readUInt32BE(0) % 100;
}
function isEnabled(
flag: string,
userId: string,
percent: number,
) {
return bucket(flag, userId) < percent;
}A user whose bucket is 7 is in at 10% and stays in at 25%, 50% and 100%, so raising the percentage only ever adds users. Putting the flag's name in the hash gives each flag a different 10%, so the same users aren't the test group for every new feature. You rarely write this yourself: Unleash's gradual rollout strategy, and the equivalents in other flag services, do the same job, with a 'stickiness' setting that picks which field to hash.
The kill switch
Suppose dark mode crashes the app on older phones. A normal fix means reverting the change, building, testing and deploying again, which can take minutes or hours. With the flag, Kitty turns it off, and every user is back on the old theme as soon as the apps pick up the new value, usually within seconds.
Some teams keep a flag permanently for this reason. A flag that can turn off an expensive recommendations panel, or move payments to a backup provider, is an operational control, and it has to be in place before the incident starts.
A kill switch only rolls back the code path behind the flag. It can't undo a database migration the new feature ran, or data it wrote in a new format. If the old code can't read what the new code wrote, turning the flag off breaks things in a different way. Build the new path so the old one keeps working alongside it.
Where flags live
A flag can be as simple as an environment variable read at start-up. Then changing it means restarting the app, and you can't target individual users. Most teams that use flags seriously run a flag service.
Unleash, the open-source tool from the video, is one. It has a server with a dashboard where you create flags and set their rules, and SDKs for most languages that your app calls to ask whether a flag is on. A server-side SDK fetches the rules from Unleash in the background, keeps them in memory and evaluates each check locally, so a flag check doesn't cost a network request. Commercial services such as LaunchDarkly work the same way, and OpenFeature is a vendor-neutral standard API, so your code can talk to any of them through one interface.
Whatever you use, decide what happens when the flag service can't be reached. Every check needs a safe default, nearly always 'off', so an outage in the flag service never switches on a half-finished feature.
Kinds of flags
Not every flag is a temporary release switch:
- Release flags hide unfinished or unproven work. They should live for days or weeks, then be removed.
- Experiment flags split users between variants for an A/B test, and stay until the result is in.
- Ops flags are kill switches and load controls, sometimes kept for good.
- Permission flags turn features on for certain users, such as beta testers or a paid plan.
The kind tells you how long a flag should live and who should be allowed to change it.
Common mistakes
- Never removing them: every flag doubles the paths through the code it guards. Once a feature is at 100% and stable, delete the flag and the old branch. A codebase with hundreds of stale flags is hard to read and harder to test.
- Testing only one combination: with several flags live at once, the mix in production may be one nobody tested. At the very least, test each new flag both on and off.
- Treating a flag as security: a flag that hides a button in the browser doesn't stop anyone calling the API behind it. Anything a user mustn't reach needs a real check on the server.
- Reusing a flag's name: giving an old flag's name to a new feature can switch the new one on for users still matched by the old rules. Names cost nothing: make a new one.
- Flags inside flags: a check nested inside another check is hard to reason about. Keep each flag guarding one clear piece of behaviour.
When to use it
Feature flags pay off when you deploy often and want to try changes on a slice of users before everyone sees them. They help less on a small project with one developer and no users yet, where a flag service is extra moving parts for little gain. A small change you'd happily roll back with a normal deploy doesn't need one either.
Key takeaways
- A feature flag is a runtime switch around a code path, changed without redeploying.
- Flags separate deploying code from releasing a feature, so unfinished work can ship switched off.
- Gradual rollouts hash the user's id, so each user keeps the same answer as the percentage grows.
- Turning a flag off quickly rolls back the code path, but not data the feature changed.
- Remove release flags once the feature is fully out, or they turn into debt.