Skip to content
Cloud6 min read

Deploys that can be undone, and the ones that cannot

The question that matters about a deployment strategy is not how fast it ships but how quickly a bad change stops affecting users. Most teams can deploy in minutes and cannot roll back at all — because a migration ran, or because rollback was never tested, and both are discovered at the worst possible time.

Part ofCloud, Hosting & IT Infrastructure

Every deployment approach is a position on one trade-off: how much exposure a bad version gets before it is noticed, against how much infrastructure and discipline the approach demands. Choosing sensibly requires knowing which failure modes each one actually prevents.

The four approaches, compared honestly

StrategyExposure to a bad versionRollbackNeeds
RecreateAll users, plus downtimeRedeploy the old versionNothing; acceptable for internal tools
RollingA growing share as instances updateRoll forward or reverse the rolloutMultiple instances, health checks
Blue-greenAll users at cutover, none beforeInstant — switch traffic backDouble capacity during the switch
CanaryA small share, then wideningStop and shift traffic backTraffic splitting and per-version metrics

Blue-green and canary solve different problems, which is why the choice between them is not about sophistication. Blue-green makes the reversal instantaneous but exposes everyone the moment you cut over. Canary limits who is exposed but means running two versions simultaneously, which your data layer has to tolerate.

Database migrations are what break rollback

Application code rolls back cleanly because it is stateless. Schema changes do not, because the old code cannot read the new shape and the data has already moved. This is the single reason most rollback plans do not work.

sql
-- Cannot be rolled back. The old version breaks immediately on
-- deploy, and reversing the migration loses every value written
-- since it ran.
ALTER TABLE users RENAME COLUMN email TO email_address;

-- Expand and contract, over three deploys. Every intermediate
-- state is compatible with the version before and after it.

-- Deploy 1 (expand): add, do not remove. Old code ignores it.
ALTER TABLE users ADD COLUMN email_address text;
-- backfill, then keep both columns written by the application

-- Deploy 2 (migrate reads): new code reads the new column.
-- Rollback is still safe, because the old column is still current.

-- Deploy 3 (contract): only once nothing reads the old column,
-- and only after deploy 2 has been stable long enough that you
-- would not roll back past it.
ALTER TABLE users DROP COLUMN email;
  • Additive changes are safe — a new nullable column, a new table, a new index
  • Destructive changes need a separate later deploy — drops, renames, type narrowing, adding NOT NULL
  • Every intermediate state must be readable by both versions, since during a rolling deploy both are running
  • Deploy 3 is the one that gets forgotten, leaving a permanently half-migrated schema that the next person has to decipher

Separate deploying from releasing

Deploying ships code. Releasing exposes behaviour. Conflating them means every change to what users see requires a deployment, and every deployment risks changing what users see. A flag decouples them, which changes the risk profile of shipping entirely.

  • Disable a broken feature in seconds, without a build, a deploy or a rollback
  • Ship incomplete work safely, behind an off flag, instead of on a long-lived branch that diverges
  • Enable for a subset first — internal users, then a small percentage — which is canary at the feature level rather than the deploy level
  • Keep the flag evaluated server-side where the feature has security or data implications, since a client-side flag is a suggestion

What to verify before calling a deploy healthy

"The deploy succeeded" means the process exited zero. It says nothing about whether the application works, and the gap between those two facts is where most incidents begin.

  1. A real health check, which exercises the dependencies the service needs — not an endpoint returning 200 because the process started.
  2. Error rate compared against the pre-deploy baseline, not against an absolute threshold. A rise from 0.1% to 2% breaches no alert and is a serious regression.
  3. Latency at the 95th percentile, per version where possible, because a change that doubles response time rarely triggers an error.
  4. One critical user journey, executed end to end. A synthetic check that signs in and completes the primary action catches what no infrastructure metric reveals.
  5. Saturation of the constrained resources, since a change in connection or memory behaviour manifests minutes after deploy rather than immediately.

Point two is where automated rollback earns its place. A deploy gate comparing error rate and latency against the previous version for a few minutes, with automatic reversal on regression, catches the large majority of bad deploys before anyone opens a dashboard.

Choosing, for a system of ordinary size

SituationReasonable choice
Static site or prerendered frontendAtomic deploy with instant rollback — this is close to free
Single service, some tolerance for brief disruptionRolling, with health checks and a tested rollback
Revenue-critical path, downtime unacceptableBlue-green, and accept the doubled capacity at cutover
Large user base, changes with uncertain impactCanary with per-version metrics and an automated gate
Any of the above, with schema changesExpand and contract, regardless of the strategy chosen

The last row is the one to take away. The deployment strategy determines who is exposed to a bad version; the migration discipline determines whether you can undo it at all. Teams routinely invest in the first and skip the second, then discover during an incident that the sophisticated rollout has nothing to roll back to.

Questions

Why can't we roll back a deploy that included a database migration?

Because application code is stateless and schema is not. A rename or a drop means the old code cannot read the new shape, and reversing the migration loses everything written since it ran. Use expand and contract: add without removing in one deploy, switch reads in a second, and drop the old structure in a third — only once you would no longer roll back past the second.

Blue-green or canary?

They solve different problems. Blue-green makes reversal instantaneous but exposes every user at the moment you cut over, and needs double capacity during the switch. Canary limits exposure to a small share first but requires running two versions simultaneously, which your data layer must tolerate, and per-version metrics — without those you have a slow rollout, not a canary, because the bad signal is diluted into the good one.

Are feature flags worth the complexity?

Yes, for separating deploy from release: you can disable a broken feature in seconds without a build, ship incomplete work safely, and enable for a subset of users first. But each flag doubles the code paths, so give every one an owner and a removal date at creation. A flag still present a year after launch is a branch nobody is testing.

How do we know a deploy is actually healthy?

Compare error rate and 95th-percentile latency against the pre-deploy baseline rather than against absolute thresholds — a rise from 0.1% to 2% breaches no alert and is a serious regression. Add a health check that exercises real dependencies rather than confirming the process started, a synthetic check completing one critical journey end to end, and saturation metrics, since connection and memory changes surface minutes later.

Want this applied to your systems?

Most of what is described here begins as an assessment: a fixed-scope review producing a prioritised written report you can act on with any supplier.

Prefer email? hello@kornspire.com