A rollback is not a delete
August 3, 2026 · Pushpal
Every update system has a rollback button, and most of them are lying to you about what it does.
What operators think it means: "undo the bad release." What it usually means: "stop offering the bad release to devices that do not have it yet." Those are very different promises, and the gap between them is exactly the set of devices that already installed the bad build. They are the reason you rolled back. They are also, in most systems, the population nothing happens to.
How the industry got here
In the mirror-and-package era, rollback genuinely was a delete. You removed the bad package from the repository, restored the previous one, and machines that had already upgraded were someone's ssh problem. Configuration management (CFEngine, Puppet, Chef) softened this by making state convergent: declare the old version, and agents walked themselves back. But convergence assumed the machine was still healthy enough to run an agent, which is precisely what a bad release endangers.
Mobile app stores made rollback WORSE, structurally. Neither Google Play nor the App Store lets a user downgrade; when a developer pulls a release, existing installs keep it forever. The industry answer became an idiom: the re-release. You take the last good build, stamp a higher version number on it, and ship 2.1.2 that is byte-identical to 2.0.0. It works, and every mobile team has done it, but notice what it is: a workaround for version monotonicity that the platform imposes, executed manually, during an incident, by someone whose hands are shaking.
Kubernetes is the counterexample worth studying. kubectl rollout undo is
not a delete and not a re-release; it is a redeploy of a previous known
state, and the system converges on it the same way it converges on
anything. Rollback being a first-class, boring operation rather than an
emergency ritual is one of the underrated reasons the k8s deployment model
won. The lesson: rollback should be a state transition the system
understands, with defined behavior for every device, including the ones
already on the pulled build.
The three honest questions
Designing rollback for a device fleet, you owe answers to three questions most systems dodge.
What do stranded devices fall back to? Our answer: the newest
still-published release below the pulled one, served as an explicit
downgrade. The manifest carries allowDowngrade: true so clients that
guard against version regression (electron-updater does, by default) know
this one is intentional. Serving-side, this is one exception in the
decision walk: normally the walk stops at the device's current version,
but if the release matching the current version is rolled back, the walk
continues below it. A pulled build should never silently keep its victims.
Client capability matters here, and pretending otherwise is how you get
surprised: our CLI applies downgrades natively, electron-updater needs its
allowDowngrade flag enabled, and Tauri's updater refuses to move
down-version at all, in which case you are back to the re-release idiom,
publish a higher-numbered build with the old contents. The system should
tell you which case you are in, not leave it as a footnote you discover
during the incident.
What does the operator see? Counts, not vibes. The rolled-back release row shows how many devices still report the pulled version and exactly what they will be offered: "61 devices stranded on this pulled build, falling back to 2.1.0 (served as a downgrade)." And critically, the rollback confirmation dialog shows the same numbers BEFORE you commit, because the moment you press that button is the worst possible moment to be doing mental arithmetic about consequences. If no published release exists below the pulled one, the dialog says so in amber: these devices will hold until you publish something. That is an ugly truth, and an ugly truth in the confirm dialog beats a discovery a week later.
Can rollback be undone? No, and this is a feature. A rolled-back release in Relayer can never be re-served; if the build was actually fine, you publish it again as a new version. Play works the same way, staged rollout percentages only move forward, halting and pulling are separate verbs. The reasoning generalizes: any control whose meaning is "this artifact was withdrawn for cause" must be irreversible, because every process downstream of it (incident notes, audit trails, the fallback behavior of thousands of devices) depends on it not quietly flipping back. For the same reason, rollout percentages in our system only increase while a release is published. Cohort bucketing is sticky, so lowering the number never un-ships anything; all it does is make the dashboard misreport reality. A control that cannot do what it appears to do should not exist.
The test
Here is a one-question audit for whatever update system you use today. Take your most recent release, imagine it is bad, and ask: for a device that already installed it, what exactly does the next update check return after I roll back?
If the answer is "nothing, they keep the bad build until something newer ships", you have a stop-the-bleeding button, not a rollback. That might be an acceptable trade for your product. But it should be a decision you made, written down where the on-call can read it, not a default you inherited from a system that never asked itself the question. Rollback is a state transition, not an apology, and the devices already on the bad build are part of the state.