Feature-flag platforms, canary releases, progressive rollouts: these are real answers for teams of fifty, where somebody owns the deploy pipeline as their full-time job. They are not wrong, they are just sized wrong. A solo developer paying monthly for a flag platform to protect a site with four hundred daily visitors has bought a fire engine to guard a candle. What a small team actually needs is cheaper and mostly free: rollback as a decision protocol, plus three or four mechanisms built from tools already in the project — a git repository, an auto-deploying host, a Postgres database.
The protocol matters more than the mechanisms, and here is why. During an incident your judgment is at its worst. You are tired, support emails are arriving, and the temptation to debug live in production is enormous. Every decision you can make in advance is a decision the impaired 2am version of you does not have to make. So the protocol comes first, written down, before anything breaks.
It starts with one question: how bad is it? Three buckets cover almost every incident.
- Down: the core flow fails for everyone — pages return 500, login is broken, checkout errors. Roll back immediately. Do not debug first.
- Degraded: one feature is broken but the rest of the site works. Kill-switch the feature if you can, or roll back if it touches money or user data.
- Cosmetic: a misaligned layout, a typo, a wrong colour in dark mode. Fix forward at a normal pace. Rolling back for this is theatre.
For the first two buckets, apply the ten-minute rule: if the cause and the fix are not both obvious within ten minutes, roll back first and debug afterwards. The logic is simple once you see it. The incident clock runs on user time, not your time. Rolling back ends the user-facing pain in minutes; debugging in production extends it for as long as the bug stays clever. Rolling back is not admitting defeat — it is separating two different jobs. Restoring service is urgent and mechanical. Finding the bug is slow and analytical. Doing them at the same time means doing both badly.
All of this assumes you know something is wrong in the first place. If your first signal of a broken deploy is an angry email the next morning, the decision tree never even runs. Error tracking and a couple of alerts are the prerequisite for everything in this article — I covered the minimal setup in observability for a solo SaaS. Knowing when to roll back is half the skill.
Modern hosts treat every deployment as an immutable artifact. On Vercel, production is just a pointer to one of those artifacts, which means rolling back is moving the pointer: open the Deployments tab, find the last good deployment, choose Promote to Production. The CLI version is one command, vercel rollback. Either way it takes seconds, because nothing rebuilds — the old build still exists exactly as it was. No git surgery, no waiting for CI, no chance of a merge conflict at the worst possible moment.
The same concept exists almost everywhere. Netlify lets you publish any older deploy. Cloudflare Pages has a rollback action on previous deployments. Render and Fly keep a history of releases you can redeploy. If you self-host with Docker, the equivalent is keeping the previous image tag around and pointing the service back at it. The mental model is identical in every case: deploys are artifacts you point traffic at, not servers you mutate.
One caveat, and it bites people. Platform rollback fixes production but does not touch your repository. The bad commit is still sitting on main, which means the next push — possibly by a future you who has forgotten all about tonight — redeploys the bug. So the platform rollback is step one, not the whole answer. It stops the bleeding in seconds; the git fix in the next section makes the repair permanent. This site's own runbook is written in exactly that order: the Vercel deployments page is line one, the revert is line two.
Git gives you two ways to undo a commit, and only one of them is safe on a branch other people or other machines pull from. git reset rewrites history. To publish it you need a force-push, which breaks every clone of the branch, confuses any CI configured against it, and on a shared repository reliably manufactures a second incident on top of the first. git revert does the opposite: it creates a new commit that applies the inverse of the bad one. History stays intact, every clone fast-forwards cleanly, and nothing about the pipeline needs to know anything unusual happened. On a shared, auto-deploying branch, revert is the only correct answer.
# undo the most recent commit on main
git revert HEAD --no-edit
git push origin main
# undo a specific commit further back
git revert abc1234 --no-edit
# undo a merged PR — merge commits need a parent (-m 1 = mainline)
git revert -m 1 def5678 --no-editBecause your branch auto-deploys, the revert is the deploy. Push it, CI runs, and the fix is live in however long your build takes — typically two to five minutes for a Next.js app. That build time is exactly why this is mechanism two and not mechanism one. The platform rollback restores service in seconds; the revert repairs the repository in minutes. Do both, in that order, and the incident is genuinely over instead of merely paused.
When the bug is fixed and you want the feature back, revert the revert: git revert applied to the revert commit reapplies the original change, and you can land the actual fix on top of it in a branch. Do not cherry-pick the original bad commit back by hand — you will bring back exactly the code that broke, minus the context of why.
This is the part most rollback articles skip, because it is the part where the honest answer is uncomfortable. Code rolls back in seconds because deployments are immutable artifacts. Your schema is not an artifact — it is shared mutable state, and there is no previous deployment of your data. If the bad deploy dropped a column, promoting yesterday's build does not bring the data back. If it renamed a column, the old code you just rolled back to now crashes too, because the column it expects no longer exists. You have rolled back into a different outage.
The fix is not a tool, it is a discipline: expand-and-contract migrations. You never change schema in place. You change it in small additive steps, each one independently deployable and each one safe to roll back from.
- Expand: add the new column or table. Purely additive — the running code does not know it exists and does not care.
- Dual-write: deploy code that writes to both old and new, and reads from the new column with a fallback to the old one.
- Backfill: copy historical data into the new column with a script or a single UPDATE, at whatever pace your database tolerates.
- Contract: days later, once the new path has proven stable, deploy code that stops touching the old column. Later still, a final migration drops it.
// step 2 of expand-and-contract: dual-write until the backfill
// and contract steps are done, then delete the old field here
await prisma.user.update({
where: { id },
data: {
name: input.name, // old column — still read by old code
displayName: input.name, // new column — the future
},
});Walk through what a rollback looks like at each stage and you see why this works. Roll back during the dual-write step and the old code reads the old column, which is still being written — nothing breaks. Roll back after the backfill and the same is true. The only deploy that can strand you is the final drop, and by then no code on any deployable commit references the old column. The cost is real: a rename that used to be one migration is now four small deploys spread over a week or two. The benefit is that no single deploy ever requires code and schema to move in lockstep, which is precisely the situation where rollback becomes impossible.
warning
A destructive migration must never travel in the same deploy as a code change. Dropping or renaming a column rides alone, days after the last code that needed it is gone — a deliberately boring deploy that touches nothing else. If your pipeline runs migrations automatically on every deploy, this rule is the one thing standing between you and an unrecoverable rollback.
The mechanics of writing and running these migrations — migrate dev, migrate deploy, what goes in the migrations folder — are covered in the Prisma guide. Expand-and-contract is the strategy layer above those mechanics, and it applies whatever migration tool you use.
Sometimes a full rollback is too blunt. The new feature is misbehaving, but the same deploy carried three unrelated fixes you want to keep. This is the exact case feature-flag platforms exist for — and the ninety-percent version of it is an environment variable and an if statement.
// kill switches: flip the env var in the host dashboard to disable
export const flags = {
aiSearch: process.env.FEATURE_AI_SEARCH === "1",
newCheckout: process.env.FEATURE_NEW_CHECKOUT === "1",
} as const;import { flags } from "@/lib/flags";
import { AiSearch, ClassicSearch } from "@/components/search";
export default function SearchPage() {
if (!flags.aiSearch) return <ClassicSearch />;
return <AiSearch />;
}Disabling the feature now takes about a minute: change the variable in the Vercel dashboard, trigger a redeploy, done. No SDK in your bundle, no network call on every request to ask a third party whether a feature is on, no monthly bill, no new external dependency that can itself go down. The trade-offs are equally plain: no per-user targeting, no percentage rollouts, and the flip requires a redeploy because env vars are baked in at build time. The day you genuinely need to release something to five percent of users and watch the error rate, a flag platform starts earning its price. For a small team shipping one or two risky features a quarter, that day is further away than the vendors suggest.
tip
Guard the entry point, not the internals. One check where the feature is reached — the route, the nav link, the API handler — beats conditionals sprinkled through five components. Killing the feature should be one flag flip, and deleting the flag once the feature is proven should be one small diff.
A rollback procedure you have never executed is a hypothesis, not a procedure. So run it once, deliberately, on a preview or staging deployment where nothing is at stake. Ship a commit that throws on the home route. Start a timer. Find your platform's rollback button and promote the previous deployment. Revert the commit, push it, watch the pipeline redeploy. Flip a kill switch and confirm the feature disappears. The first run, you will fumble — you will hunt for the promote button, second-guess the revert syntax, forget which env var controls what. Better to find that out on a Tuesday afternoon than during the real thing. Fifteen minutes of practice converts the 2am incident from a design exercise into a checklist.
Then write the checklist down where you will actually find it: a RUNBOOK.md in the repository root, ten lines, no prose. This site's own version reads roughly like this.
- 1. Vercel → Deployments → last green deployment → Promote to Production. Confirm the site recovers.
- 2. git revert the bad commit on main, push, let the pipeline redeploy. Production is now permanently fixed.
- 3. If a migration shipped with the deploy: confirm it was additive before reverting code. If it was destructive, stop — the path is a backup restore, and rushing makes it worse.
- 4. Kill switches live in lib/flags.ts. Flip the env var in Vercel and redeploy to disable a single feature.
Notice what the runbook does not contain: diagnosis. There is no step that says figure out what went wrong. That happens afterwards, with coffee, in daylight.
Once service is restored and the bug is fixed calmly, spend five minutes on the habit that makes the next incident shorter: a five-line postmortem in a POSTMORTEMS.md file. What broke. How you found out. Minutes from detection to restored service. The root cause. The one change that prevents a repeat. That is the whole template — no blame, no committee, no document nobody reads. Its value compounds: after the third entry where the answer to how you found out is a user emailed me, you know with certainty that your next investment is alerting, not code. The file turns isolated bad nights into a pattern you can act on.
Here is what to do with all of this today, while nothing is broken. Write the three-bucket decision tree and the ten-minute rule into your runbook. Open your host's dashboard and click through to the rollback action, stopping one confirm short of pressing it, so your hands know the path. Look at your most recent migration and ask the uncomfortable question: if I rolled the code back to yesterday right now, would it survive the current schema? If the answer is no, your next schema change is the right moment to adopt expand-and-contract.
Rollback is one slice of a larger discipline — environment variables, CI, monitoring, and domains all have the same property that preparing them is cheap and improvising them is expensive. The full picture is in shipping a Next.js app to production, which treats this article as one station on that path. Prepare the protocol once, drill it once, and the 2am deploy that goes wrong becomes a fifteen-minute interruption instead of a night you remember for years.
