How to Roll Back a Bad Deployment
You shipped, error rates went up, and someone is asking whether to roll back. The reason this question causes arguments at 11pm is that everyone is answering a different one. 'Can we revert the code' is nearly always yes. 'Can we revert the system to the state it was in' is a much harder question, and the difference is usually a database migration.
I'm Ajay, I build PandaStack. Here's the decision, in the order you should make it, and the preparation that turns rollback from a judgement call into a button.
Step 1: decide, in under two minutes
Roll back when the fix isn't obvious, when user-facing impact is ongoing, or when you're not certain what broke. Rolling back is a way of buying time to think, and time to think is the scarcest thing in an incident.
Fix forward when the cause is unambiguous and one-line, when the deploy included a migration that can't be undone, or when rolling back would itself be risky — a rollback that hasn't been rehearsed is a change too, and changes during incidents are how a bad hour becomes a bad day.
Step 2: the mechanics
On a platform with immutable deployments, rollback should be a single command that promotes a previous build. The important property is that it redeploys the exact artifact — the same commit, the same dependency versions — rather than rebuilding from a git revert, because a rebuild resolves dependencies afresh and may not produce what ran before.
# List deployments and find the last known-good one
pandastack app deploys <app-id>
# Roll back to the previous live deployment
pandastack app rollback <app-id>
# Or target a specific deployment via the API
curl -X POST https://api.pandastack.ai/v1/apps/<app-id>/rollback \
-H "Authorization: Bearer $PANDASTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"deployment_id": "<known-good-deployment-id>"}'Under a blue-green model, this rebuilds the old version alongside the broken one, health-checks it, and only then moves traffic — so a failed rollback leaves you where you already were rather than somewhere new. That property matters more than rollback speed. A rollback that can itself fail open is worse than no rollback.
If you're on plain containers or VMs, the equivalent is redeploying the previous image tag or the previous release directory. The requirement is the same either way: previous versions must still exist and be startable. A platform that keeps only the current build has no rollback, whatever the dashboard says.
Step 3: the database, which is the actual problem
Application code is stateless and reverts cleanly. Your database does not. If the deploy ran a migration, rolling code back leaves new code's schema under old code, and old code will hit columns it doesn't know about, missing columns it expects, or constraints it can't satisfy.
The two safe positions are: the migration is backwards-compatible, so old code runs fine against the new schema; or you have a down migration you've actually tested against production-shaped data. 'The ORM can generate a down migration' is not the second thing.
- Additive changes — a new nullable column, a new table, a new index — are safe to roll back over. Old code ignores what it doesn't know about.
- Destructive changes — dropped or renamed columns, tightened constraints, changed types — are not. Rolling back over these needs a restore, not a revert.
- Data migrations that transform rows in place are the worst case: even a perfect down migration can't recover information the up migration discarded.
This is why the expand-and-contract pattern is worth the discipline. Deploy the schema change first and separately, in a form both old and new code tolerate. Deploy the code. Remove the old column in a later, separate deploy, once you're confident. Each step is independently reversible, which is the whole point — rollback stops being a special operation and becomes a normal one.
Step 4: verify, then write it down
- Confirm the rollback actually took effect. Check the deployed commit, not the dashboard's status badge. A rollback that silently no-oped is a real failure mode.
- Watch the metric that told you something was wrong. If error rate was the signal, error rate is the confirmation — not 'the page loads for me'.
- Check for partial state. Jobs that started under the new version, cache entries written in a new format, webhook deliveries already sent. These outlive the rollback.
- Tell people it's done, in the same channel where you said it was broken.
- Only then look at what happened. The temptation to debug while the site is down is strong and wrong.
Making rollback boring
Everything above is easier if you did these things in advance. None of them are exotic.
- Keep previous deployments startable, not just recorded. Check how many your platform retains and whether promoting one is a single action.
- Separate schema changes from code changes into different deploys. This is the single highest-leverage habit in this whole list.
- Make migrations backwards-compatible by default, and treat a destructive migration as a deliberate, reviewed exception.
- Rehearse a rollback on staging quarterly. An untested rollback path is a plan, not a capability.
- Have a database restore point you trust, and know its age. For per-environment databases, a clone from a point in time before the deploy is often faster and safer than a down migration.
- Write the health check so it actually checks something. An endpoint that returns 200 because the process is alive will happily certify a broken deployment.
# When a migration can't be reversed, restore is the honest path.
# Clone to a point in time before the deploy, verify against the clone,
# and only then decide what to do with production.
pandastack db clone prod-db \
--label pre-deploy-check \
--target-time 2026-08-19T14:05:00Z
# The source database is untouched; you get an independent copy to inspect.Short version
Roll back when you're unsure — it buys thinking time. Fix forward when the cause is obvious and the migration isn't reversible. Promote a previous artifact rather than rebuilding a revert. And the real work isn't the rollback command, it's the habit of shipping schema changes separately from code, so that reverting one never requires reverting the other.
Frequently asked questions
Should I roll back or fix forward?
Roll back when the cause is not obvious, when users are actively affected, or when the fix would take longer than reverting. Rolling back is primarily a way to buy time to think, and thinking time is the scarcest resource in an incident. Fix forward when the cause is unambiguous and the change is small, when the deploy included a migration that cannot be safely undone, or when the rollback path itself has never been tested — an unrehearsed rollback is a change made under pressure, and changes made under pressure are how a contained problem spreads. If you cannot answer within a couple of minutes whether the deploy included a schema migration, finding that out is the first step either way.
Why can't I roll back a deployment that included a database migration?
Because code is stateless and your database is not. Rolling the application back restores old code, but the schema stays where the migration left it, so old code meets a database it does not understand — columns it never knew about, columns it expects that were renamed, constraints it cannot satisfy. Additive migrations are usually safe to roll back over, since old code simply ignores a new nullable column or table. Destructive ones are not: dropping a column, renaming it, tightening a constraint, or transforming rows in place all leave old code broken, and in the last case the information needed to reverse it may no longer exist anywhere except a backup.
What is the expand and contract pattern for migrations?
It splits a schema change into three separate deployments so that every intermediate state works with both old and new code. First expand: add the new column or table without removing anything, and deploy that alone. Then deploy the application code that writes to and reads from the new shape, backfilling existing rows if needed. Finally, in a later deploy once you are confident, contract by removing the old column. Each step is independently reversible because at no point does running code depend on something that has just been removed. It is more deploys and more discipline, and it is what makes rollback an ordinary operation rather than an incident within an incident.
How do I know if a rollback worked?
Check the deployed commit or artifact identifier rather than a status badge, because a rollback that silently no-opped is a genuine failure mode and the dashboard will often look fine. Then watch the specific metric that told you something was wrong — if error rate was the signal, error rate is the confirmation, not whether the homepage loads for you. Finally, look for state that outlived the rollback: background jobs started under the broken version, cache entries written in a new format, webhooks already delivered, or partially written rows. Those do not revert with the code, and they are the usual reason a system still misbehaves after a technically successful rollback.
How many previous deployments should a platform keep?
Enough that you can reach the last version you genuinely trust, which in practice means more than one. Teams routinely discover a problem two or three deploys after it was introduced, and if the platform retains only the immediately previous build you can only step back into another broken state. Check two things about your platform: how many prior deployments it keeps, and whether promoting one is a single action that redeploys the exact original artifact rather than rebuilding from source. A rebuild re-resolves dependencies and may not reproduce what actually ran, which is precisely the uncertainty you are trying to remove during an incident.
Keep reading
- App hosting on PandaStack — blue-green deploys with health checks and one-command rollback
- Zero-downtime schema migrations on deploy
- Blue-green deploys with microVM isolation
- Postgres point-in-time recovery, explained
- Cloning a production database for testing
- Push-to-deploy and GitHub webhooks, explained
49ms p50 cold start. Fork, snapshot, and scale to zero.