Deploy vs release: the difference feature flags make
A deploy puts new code on your servers. A release is the moment a user can see or use it. Without a feature flag, those two events are forced to happen at the same time: the build that ships is the build everyone gets. A flag breaks that link, so merging and shipping code stops being the same decision as releasing a feature.
Most teams never chose to couple deploy and release. It is just what happens by default when a build goes out and nothing in the code says who gets to see what. The rest of this post covers what each term means, why the default costs you on a bad deploy, and how a flag splits them.
Key Takeaways
- Deploy means new code reaches production. Release means a user can see or use it. Martin Fowler’s feature toggles article names this split directly.
- Without a flag, deploy and release are the same event, so a broken deploy is also a broken release for every user at once, with no way to separate the two.
- A flag set to off deploys with the next build invisible to anyone, then releases later by flipping the flag, a config change instead of a new deploy.
- A dark launch runs the new code path in production at 0% traffic before anyone decides to release it, so the first real traffic it sees is a chosen rollout, not launch day.
- The tradeoff: splitting deploy from release only covers the parts of a change a flag gates. Adding the flag itself, and anything outside it, still means deploy equals release.
Deploy vs release: two different events
A deploy is the code reaching production: the build runs, the artifact lands on the server or the edge, and the new binary is live. A release is the moment a user is exposed to the change the deploy carried. Jez Humble and David Farley’s continuous delivery practice treats these as separate concerns on purpose: a deployment pipeline can push a build through every stage up to production without that build being switched on for anyone.
Most teams never draw that line, because without something in the code to hold it open, a deploy is a release. The new code runs, and whoever hits the server next gets it. There’s no third state between “not deployed” and “live for everyone.”
// no flag: deploy is release
export function renderCheckout() {
return <NewCheckout />; // every request gets this the moment the build ships
}
That single line is the entire problem. There’s nowhere in it to say “deployed, but not yet released.”
Why the default costs you on a bad deploy
When deploy and release are the same event, a mistake in the build is a mistake for every user at once. There’s no way to let the code reach production quietly and watch it before anyone depends on it. That’s why the Friday deploy freeze exists at so many companies: a bad Friday release means a weekend incident with nobody around to catch it, so the team stops deploying instead of finding a way to deploy without releasing.
The freeze treats the symptom. “Ship the code” and “show the feature” were never two separate decisions to begin with, so the only lever left is not shipping at all.
Splitting deploy from release with a feature flag
A feature flag reintroduces the missing state. The code deploys with the flag off, runs in production, and shows nobody anything different. Releasing it later is a config change, not a new deploy.
---
id: new-dashboard
status: active
owner: priya@acme.com
surface: dashboard
variants:
- id: "off"
weight: 100
- id: "on"
weight: 0
---
## Brief
Ship dark with the next deploy. Flip to variant "on" once the
support team confirms the data matches the old dashboard.
That file merges and deploys with the rest of the build, same as any other change. Nothing about the deploy itself changed. What changed is that “live in production” and “visible to a user” are no longer the same checkbox. dif build compiles the active flags into a typed client, so the call site is one function instead of a hand-rolled check:
if (dif("new-dashboard") === "on") {
return <NewDashboard />;
}
return <LegacyDashboard />;
Releasing it is editing the weights to 0/100 and merging, a commit that flips visibility without touching the code path at all. The .md format covers the rest of what the frontmatter can hold.
Dark launches: releasing to nobody first
A flag held at 0% is a dark launch: the code runs in production, under real load, for zero real users. Facebook coined the term for exactly this. Before Chat launched in 2008, the team ran a “dark launch” period where pages connected to the chat servers and simulated real traffic with no UI element drawn on the page, so the backend had already proven itself under load before a single user saw a chat window.
$ dif build
compiled 4 active flags -> dif/client.ts
new-dashboard: variant "on" at weight 0, surface dashboard
The dashboard is deployed. dif build’s own output confirms it compiled into the running client. Nobody sees it, because the weight says so, not because the deploy was held back.
The tradeoff: what still doesn’t decouple
A flag only decouples the parts of a change it gates. The flag file itself still has to be deployed before anyone can flip it, so the very first time a feature ships, deploy and release are briefly the same event again: the moment the flag exists in production at weight: 0.
And a flag doesn’t make the toggle instant. Turning a release off once it’s live is editing a number and merging, which is a commit and a deploy, not a sub-second dashboard flip. If your build takes ten minutes, your worst-case time to pull a release back is ten minutes. For a payment path that needs an instant kill switch, that cost is real, and a flag held in git is the wrong tool for that one switch even if it’s the right tool for everything else.
Deploy vs release in CI/CD
In a CI/CD pipeline, the split shows up in what the pipeline can and can’t guarantee. It can promise that a build passing every stage reaches production. It can’t promise what a user sees, because that’s a flag now, not a deploy step.
dif validate runs as a CI check on every merge: it confirms a flag’s weights sum to 100, its owner is a real email, and the code doesn’t reference a flag that no longer exists. A broken flag fails the pull request the same way a broken test does, so the release decision stays reviewable even though it’s separate from the deploy.
FAQ
What is the difference between deploy and release? A deploy is new code reaching production. A release is the moment a user can see or use it. Without a feature flag, the two happen together; with one, they’re separate decisions made at different times.
Is continuous deployment the same as continuous release? No. Continuous deployment means every build that passes CI reaches production automatically. It says nothing about who sees the new code once it’s there, which is exactly the job a feature flag does.
Can you deploy code without releasing it? Yes, that’s what a feature flag held at 0% does. The code ships with the next build and runs in production, but no user is exposed to it until the flag is flipped.
What is a dark launch? A dark launch is code that’s deployed and running in production at 0% visibility, usually to test it under real load before anyone decides to release it. The flag or rollout is what keeps it dark.
Do you need feature flags to decouple deploy from release? A flag is the common way to do it, but it’s not the only one. Branch by abstraction, a separate preview environment, or routing rules in front of the server can all hold a release back from a deploy. A flag is usually the cheapest of these because it needs no new infrastructure, just a file and a check at the call site.
Getting started
Deploy vs release comes down to one question: does your code have anywhere to say “live, but not visible yet”? Without that state, shipping a build and releasing a feature are forced into the same moment, and a bad deploy is a bad release for everyone at once. A feature flag gives you that state back, so a release becomes a decision you make on purpose instead of a side effect of the next build.
Install the CLI and gate your next deploy instead of letting it double as the release:
npm install -g @dif.sh/cli
dif init
See feature flags in git for the full workflow once flags live in your repo, or release trains for what changes when the release schedule itself goes away. The feature flags page covers the rest of the model.