How to Roll Back a Bad Deploy Without Making It Worse

The rollback button is the most confidently described and least frequently tested part of most deployment setups. Teams will tell you they can roll back in seconds. Ask when they last did it, on purpose, in production, and the answer is usually “we have not needed to”.

Then a Friday afternoon deploy takes checkout down, someone hits rollback, and the old code starts throwing 500s because release N dropped a column that release N minus 1 still queries. Now you have two outages: the original bug and a rollback that failed halfway.

Rollback is not an undo button. It is a claim about the compatibility between your last two releases, and that claim is only true if you designed for it.

Roll Back First, Diagnose Second

The instinct after a bad deploy is to work out what broke. Resist it. Diagnosis is unbounded work with users watching. Rollback is bounded work with a known outcome.

The order that keeps outages short is: restore service, then investigate the artefact you just removed, at your own pace, with logs and traces you can read without a manager in the channel.

That said, rollback is not always the right move. The decision comes down to whether anything irreversible has happened since the deploy.

SituationActionWhy
Application bug, no schema changeRoll backPrevious artefact is known good
Performance regression under loadRoll backRestores a known baseline instantly
Additive migration only (new nullable column)Roll backOld code ignores the new column
Destructive migration already appliedRoll forwardOld code cannot run against the new schema
Data backfill already writtenRoll forwardRollback does not undo the data
New message format already consumedRoll forwardConsumers have committed offsets
Third-party or config change outside the artefactNeitherRevert the config, not the release

The pattern in the right-hand column: rollback works when the only thing that changed was code. The moment state changed, going backwards stops being free.

The Four Things That Break Rollbacks

1. Schema changes that destroy structure

This is the big one. Dropping a column, renaming a table, adding a NOT NULL constraint, or narrowing a type all make the previous release invalid. The rollback deploys cleanly and then fails on the first query.

The fix is expand and contract, what Martin Fowler calls parallel change ↗. You split one breaking change across three releases:

Expand and Contract: Where Rollback Is Safe Release 1: Expand Add new column Write to both Release 2: Migrate Backfill data Read from new Release 3: Contract Drop old column One way door Rollback safe Rollback unsafe Time Later Keep releases 2 and 3 days apart. The gap is your rollback window, not an inconvenience to compress.

The cost is three deploys instead of one. The benefit is that for the whole window between release 1 and release 3, you can go backwards freely. Our guide to database migrations without the fear covers the mechanics of each phase.

2. Caches holding the new format

Release N starts serialising a session object with a new field layout and writes it to Redis. You roll back. Release N minus 1 reads those entries, fails to deserialise, and throws on every request from any user who was active during the bad window.

Cache entries outlive deploys. Either version your cache keys so a rollback reads a different namespace, or make deserialisation tolerant of unknown and missing fields. Silent tolerance beats a strict parser here.

3. Queues and event consumers

The same problem with worse timing. If release N publishes messages in a new shape, rolling back the publisher does not recall the messages already in flight. The old consumer picks up a message it cannot parse and either crashes or dead-letters it.

Publishers should be rolled back before consumers, and consumers should accept both shapes for at least one release. This is the same idempotency and compatibility discipline that makes idempotent API endpoints safe to retry.

4. Client-side assets already in browsers

A single page app deploy leaves users holding the old JavaScript bundle, which requests API endpoints that may have changed. Rolling back the server while clients hold the new bundle produces a version mismatch that looks like random errors. Keep old asset versions available rather than purging them on deploy, and give the client a way to detect a version skew and reload.

Make the Rollback Path Boring

A rollback that requires thought during an incident is a rollback that will not happen. Three properties make it dependable.

Immutable, addressable artefacts. The previous release must exist as a built artefact you can redeploy by digest, not a commit you rebuild from source. If rollback triggers a fresh CI run, your recovery time is your build time. This is one of the design constraints worth baking into your CI/CD pipeline from the start.

One command, one operator. Not a sequence of five steps across two dashboards. Whoever is on call should be able to run it without asking anyone.

Automatic triggers where the signal is clean. If error rate exceeds a threshold within ten minutes of a deploy, roll back without a human in the loop. This only works if your alerting is trustworthy, which is a question of observability rather than monitoring: you need to distinguish a genuine regression from a noisy dependency.

Reduce What You Need to Roll Back

The best rollback is one that only affects a fraction of users. Two techniques do most of the work.

Progressive delivery. Route a small slice of traffic to the new release and compare it against the old one before going wide. Google’s SRE workbook chapter on canarying releases ↗ is the clearest treatment of choosing the canary population and the evaluation window. The related tradeoffs across blue-green, canary and rolling updates are covered in our post on deployment strategies.

Decouple release from deploy. Ship the code dark behind a flag, then enable it separately. Turning off a flag is faster and safer than redeploying, and it does not touch the artefact at all. Feature flags turn most rollbacks into a config change, though they come with their own cleanup debt if you never remove the dead branches.

Practise It

Pick a low-traffic window. Deploy the current release again, then roll back to the previous one, on purpose, with a stopwatch. You will find out three things: whether the command works, how long it actually takes, and whether anyone other than the person who wrote it knows how to run it.

Teams that do this quarterly recover in minutes. Teams that do not discover their rollback is broken at the worst possible moment, which is how a ten minute incident becomes a ninety minute one and eats a month of error budget.

Before the next release that touches schema, cache format, or message shape, write down one line: what happens if we roll this back an hour after deploy. If the honest answer is “we are not sure”, you have found the work worth doing this sprint.

Frequently asked questions

Should you roll back or roll forward after a bad deploy?

Roll back when the previous release is known good and nothing irreversible has happened since, which covers most application-level regressions. Roll forward when a rollback would leave the system in a state the old code cannot handle, typically after a destructive database migration, a data backfill, or a message format change that consumers have already processed. The default should be rollback, because it needs no diagnosis and no new code under pressure. Rolling forward requires you to understand the bug, write a fix, and get it through the pipeline while users are affected, which is exactly the wrong time to be writing code.

Why do database migrations break rollbacks?

Because code deploys are reversible and schema changes usually are not. If release N drops a column and you roll back to release N minus 1, the old code queries a column that no longer exists and every request fails. Dropped columns, renamed tables, and narrowed constraints all destroy data or structure the previous release depends on. The fix is expand and contract: add the new structure, ship code that writes to both, migrate the data, and only drop the old structure in a later release once you are certain you will never roll back past it.

How long should a rollback take?

Under five minutes from decision to restored service, and it should be a single command or a single button. If your rollback involves rebuilding an artefact from source, it is not a rollback, it is a rebuild, and it will take as long as your slowest CI pipeline. Keep the previous release's artefact immutable and addressable by digest so restoring it is a deployment of something that already exists rather than a fresh build.

What is a rollback runbook and what should it contain?

A rollback runbook is a short document with the exact command to restore the previous release, how to confirm it worked, and the specific conditions under which rollback is unsafe for the current release. Keep it to a single page. The most valuable section is the unsafe list, because it is written by the engineer who shipped the change while they still remember which migration or cache format cannot survive going backwards.

Does a canary release remove the need for rollbacks?

No, it reduces the blast radius and makes the rollback decision earlier and cheaper. A canary exposes a small percentage of traffic to the new release, so a failure hits a fraction of users and the rollback is a traffic shift rather than a redeploy. You still need the rollback path to work, and you still need to have thought about migrations and cache formats. Canarying changes who is affected and how fast you notice, not whether going backwards is safe.

Enjoyed this article? Get more developer tips straight to your inbox.

Comments

Join the conversation. Share your experience or ask a question below.

0/1000

No comments yet. Be the first to share your thoughts.