kubectl rollout undo looks like an undo button: bad deploy, one command, back to safety. It mostly is one. But what it restores, how far back it can reach, and what happens when a GitOps controller is watching the same Deployment are all governed by mechanics worth understanding before the bad deploy. This is how Kubernetes Deployment rollbacks actually work: where revisions live, what rollout undo does and does not restore, and why in a GitOps pipeline the real rollback is a Git revert.
Revisions are ReplicaSets, not snapshots
A Deployment does not store a history of its past specs. Every time you change the pod template (spec.template: image, env vars, probes, labels on the pods), the Deployment controller computes a hash of the new template and creates a ReplicaSet named after it, then scales the new ReplicaSet up and the old one down according to the rollout strategy. The old ReplicaSet is not deleted: it stays at 0 replicas, still holding the exact pod template it was created with.
That retired ReplicaSet is the revision. The bookkeeping is a single annotation:
$ kubectl get rs -l app=my-app
NAME DESIRED CURRENT READY AGE
my-app-5d8c7b9f6d 0 0 0 2d # revision 1
my-app-7f9b4c8d5e 0 0 0 1d # revision 2
my-app-6c5d9e8f7a 3 3 3 2h # revision 3
$ kubectl get rs my-app-7f9b4c8d5e \
-o jsonpath='{.metadata.annotations.deployment\.kubernetes\.io/revision}'
2
kubectl rollout history deployment/my-app is essentially a pretty-printer over these annotations. Two consequences follow directly:
- A revision only records the pod template. Replica count, HPA settings, the Deployment’s own labels and annotations, and anything outside
spec.templateare not part of it. - If the ReplicaSet is gone, the revision is gone. There is no other copy.
One wrinkle: if you roll out a template identical to one an old ReplicaSet already holds, the controller reuses that ReplicaSet rather than creating a duplicate, and its revision annotation is bumped to the newest number (the previous numbers are kept in a deployment.kubernetes.io/revision-history annotation). Revisions deduplicate by content.
What rollout undo actually does
# back to the previous revision
kubectl rollout undo deployment/my-app
# back to a specific revision
kubectl rollout history deployment/my-app
kubectl rollout history deployment/my-app --revision=2 # inspect before jumping
kubectl rollout undo deployment/my-app --to-revision=2
rollout undo finds the target revision’s ReplicaSet, takes its pod template, and patches it back into the Deployment’s spec.template. That is all. There is no special reverse gear: the patch triggers an ordinary rolling update forward to the old template, using the Deployment’s normal strategy, probes, maxSurge and maxUnavailable. A rollback is exactly as fast, and exactly as safe, as your regular rollouts are. If your deploys drop requests mid-rollout, your rollbacks will too, which is one more reason to get your rolling deployment settings right before you need one under pressure.
The revision numbering after an undo surprises people. Rolling back to revision 2 does not “return to revision 2”: the template of revision 2 becomes the new latest revision (say, revision 4), and the history entry for 2 disappears — its content now lives at the top. Revision numbers only ever increase; rollout history is a log of templates, not a timeline you rewind.
$ kubectl rollout undo deployment/my-app --to-revision=2
deployment.apps/my-app rolled back
$ kubectl rollout history deployment/my-app
REVISION CHANGE-CAUSE
1 <none>
3 <none>
4 <none> # this is the old revision 2, renumbered
What a rollback does not restore
Because a revision is only a pod template, several things people expect from an “undo” are out of scope.
Replica count
Scaling does not create a revision: replicas sits outside spec.template, so kubectl scale leaves no trace in rollout history. Symmetrically, rollout undo never touches the replica count: if you scaled from 3 to 10 after the bad deploy, you are still at 10 after the undo. Usually that is what you want; the surprise is when someone expects undo to also revert an accidental scale-down. It will not. Scale and template are independent axes.
ConfigMaps, Secrets, and everything around the pod
The pod template references a ConfigMap or Secret by name. If the bad deploy also changed the ConfigMap’s contents, rolling back the Deployment re-mounts the same, still-changed ConfigMap. The same applies to database migrations, CRDs, RBAC changes, or anything else your release touched outside spec.template. rollout undo rolls back one field of one object.
(The standard mitigation is immutable, content-hashed names like app-config-7d9f2b, so a template rollback mechanically brings back the old config. Kustomize’s configMapGenerator and Helm’s checksum annotations both exist for this reason.)
Anything a paused Deployment did
You cannot roll back a paused Deployment; kubectl rollout undo refuses until you kubectl rollout resume it. If you use pause/resume to batch changes, remember that the paused period produces a single revision, not one per change: the undo granularity is the resume, not the edit.
revisionHistoryLimit: how far back you can reach
The Deployment controller garbage-collects old ReplicaSets beyond spec.revisionHistoryLimit, which defaults to 10. Older revisions are deleted, and with them the ability to roll back to them:
$ kubectl rollout undo deployment/my-app --to-revision=1
error: unable to find specified revision 1 in history
Two edges of the dial:
revisionHistoryLimit: 0deletes every inactive ReplicaSet immediately. It makes rollback impossible, full stop. Occasionally proposed as “cleanup”; it is the removal of your safety net.- A high limit costs almost nothing (retired ReplicaSets are small API objects with zero pods), but note that noisy pipelines burn through history fast. Ten deploys on a busy day means yesterday’s known-good revision may already be beyond reach by the time someone notices a slow regression.
Ten is a reasonable default for a repo-driven pipeline. If your rollback strategy genuinely depends on rollout undo (see below for why it maybe should not), size the limit against your deploy frequency, not the default.
The missing change-cause, and knowing what you are rolling back to
rollout history has a CHANGE-CAUSE column that is <none> on most clusters, because it is populated from the kubernetes.io/change-cause annotation, which nothing sets automatically. The old kubectl --record flag that filled it has been deprecated for years. You can set it by hand:
kubectl annotate deployment/my-app \
kubernetes.io/change-cause="release 2026-07-24, image v1.42.1"
The annotation is copied to the ReplicaSet at rollout time, so each revision keeps its own cause. In practice, hand-maintained annotations rot. The reliable way to identify a revision is to look at its actual template:
kubectl rollout history deployment/my-app --revision=3
# ...prints the pod template: image tag, env, the thing that actually changed
An undo done blind, meaning rollout undo without checking what the previous revision contains, is a gamble that the previous deploy was the only recent change. Thirty seconds of --revision=N inspection is cheap insurance; this matters double when history has been renumbered by an earlier undo.
rollout undo vs a Git revert
If your Deployments are applied by a pipeline or a GitOps controller from manifests in Git, rollout undo has a structural problem: it changes the cluster, not the repo. The Deployment in the cluster and the manifest in Git now disagree, and every mechanism you have built exists to eliminate exactly that disagreement.
- With a CI pipeline (
kubectl applyon merge): the undo holds until the next merge, at which point the pipeline re-applies the manifests, including the bad image tag you rolled away from. The rollback silently un-rolls itself, possibly days later, deployed by an unrelated change. - With a GitOps controller (Argo CD, Flux): the controller detects the drift on its next sync. With self-heal enabled, it reverts your rollback within minutes. The controller is doing its job, which is making the cluster match Git. Fighting it with repeated undos is fighting your own tooling; the only workaround is disabling auto-sync, which trades away the guarantee GitOps exists to provide.
The rollback that fits the model is a Git revert: revert the commit that changed the image tag (or the release bump), let the ordinary pipeline apply it. You keep the audit trail, the diff review, whatever CI checks guard your manifests, and, critically, the guarantee that Git still describes what runs. The trade-off is honest: a revert goes through the pipeline and takes minutes, while rollout undo acts in seconds. A defensible emergency pattern is to use rollout undo as the stopgap that stops the bleeding, then immediately push the Git revert so the repo agrees with what you just did, and treat any undo that is not followed by a commit as an incident action item, not a fix. If your pipeline is slow enough that this gap feels dangerous, the pipeline is the thing to fix; making deploys and rollbacks boring is precisely the point of industrializing your CI/CD.
Quick reference
kubectl rollout history deployment/my-app # list revisions
kubectl rollout history deployment/my-app --revision=3 # inspect one
kubectl rollout undo deployment/my-app # back one revision
kubectl rollout undo deployment/my-app --to-revision=3 # back to a specific one
kubectl rollout status deployment/my-app # watch it converge
And the gotchas in one pass: revisions are retired ReplicaSets and record only the pod template; undo is a normal rolling update forward and renumbers history; it restores neither replica count nor ConfigMaps; revisionHistoryLimit (default 10, never 0) bounds how far back you can go; a paused Deployment refuses to roll back; and under GitOps, the durable rollback is the Git revert — rollout undo is at best the first half of one. More automation material in the automatisation category.