Sooner or later a deploy pipeline fails with this error, and every retry fails the same way.
Error: UPGRADE FAILED: another operation (install/upgrade/rollback) is in progress
Nothing is actually in progress. A previous Helm run started an upgrade, wrote a release record saying so, and died before it could write the result. Helm now treats the release as locked. This guide explains how that lock works, how to clear it without losing track of what is deployed, and how to set up pipelines so it doesn't come back. The commands and flags are from Helm 4.
How Helm tracks a release
Helm keeps no state of its own on your machine. By default each revision of a release is a Kubernetes Secret in the release's namespace, named sh.helm.release.v1.<release>.v<revision>, with the type helm.sh/release.v1. The Secret holds the chart, the values and the rendered manifest. Its labels hold the release name, the revision number and the status, plus owner=helm.
Every install, upgrade and rollback creates a new revision. Helm writes that revision first with a pending status, then applies the manifest, then updates the status. There are three pending statuses: pending-install, pending-upgrade and pending-rollback.
The pending status doubles as a lock. Before an upgrade, Helm reads the latest revision. If its status is pending, Helm refuses to continue and returns the error above. The Helm source comments describe it as a pessimistic lock against two upgrades running at the same time. That works well while a Helm process is alive. When the process disappears mid-upgrade, the lock stays.
Why the process disappears
Helm 4 handles SIGINT and SIGTERM during an upgrade. When you press Ctrl+C or a CI system stops the job politely, Helm prints that the release was cancelled and records the revision as failed. A failed revision doesn't block the next upgrade.
The trouble comes from deaths Helm can't catch. A SIGKILL from a CI runner that hits its job timeout, an out-of-memory kill, a runner pod evicted from its node or a laptop that loses its network all stop Helm with no chance to write a final status. The revision stays pending forever.
Step 1: confirm what state the release is in
Start with the release status and its history.
helm status web -n shop
helm history web -n shop
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
3 Mon Nov 2 10:12:44 2026 superseded web-1.4.0 2.3.0 Upgrade complete
4 Tue Nov 3 16:40:02 2026 deployed web-1.4.1 2.3.1 Upgrade complete
5 Wed Nov 4 09:05:17 2026 pending-upgrade web-1.5.0 2.4.0 Preparing upgrade
The pattern tells you a lot. Revision 4 is still deployed, because Helm only marks the previous revision superseded once an upgrade finishes. Revision 5 is the abandoned attempt, still carrying the placeholder description Preparing upgrade.
To find every stuck release in the cluster, filter the release list.
helm list --pending --all-namespaces
Before you change anything, make sure no Helm process is really still running against this release. Check for a running pipeline job or another engineer's terminal. If one exists, wait for it. The steps below are only safe when the pending revision is truly abandoned.
Step 2: look at what the cluster is running
A half-finished upgrade can leave the cluster anywhere between revision 4 and revision 5. Some objects may be updated, others not. Look at the workloads before you pick a fix.
kubectl get deploy,statefulset,job -n shop -l app.kubernetes.io/instance=web
kubectl rollout status deployment/web -n shop
The app.kubernetes.io/instance label is a chart convention, not something Helm adds by itself, so adjust the selector if your chart labels its objects differently. What you want to know is whether the new version partly rolled out and whether it is healthy.
Step 3: roll back to the last good revision
For a stuck upgrade, a rollback is the cleanest fix. Unlike an upgrade, a rollback doesn't refuse to run when the latest revision is pending.
helm rollback web 4 -n shop --wait --timeout 10m
If you leave out the revision number, Helm rolls back to the revision just before the latest one, which here is also 4. Naming it explicitly avoids surprises when the history is longer than you think.
Helm creates a new revision 6 with revision 4's manifest and applies it. Anything the failed attempt changed gets put back, and helm history now ends with revision 6 as deployed.
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
4 Tue Nov 3 16:40:02 2026 superseded web-1.4.1 2.3.1 Upgrade complete
5 Wed Nov 4 09:05:17 2026 pending-upgrade web-1.5.0 2.4.0 Preparing upgrade
6 Wed Nov 4 11:31:50 2026 deployed web-1.4.1 2.3.1 Rollback to 4
Revision 5 keeps its pending status in the history. That's harmless. Helm only checks the latest revision when it decides whether a release is locked, and that is now revision 6. Rerun the upgrade and it goes through.
helm upgrade web ./web -n shop -f values-prod.yaml --wait --timeout 10m
Step 4: handle a stuck first install
A release whose only revision is pending-install needs a different fix. There is nothing to roll back to, so helm rollback fails because the release has no earlier revision. helm upgrade --install fails with the same "another operation" error, and a plain helm install refuses because the name is still in use.
Uninstall it and install again.
helm uninstall web -n shop --wait
helm install web ./web -n shop -f values-prod.yaml --wait --timeout 10m
The uninstall deletes whatever objects the first attempt created, using the manifest stored in the pending revision. For a release that never finished installing, that's usually what you want.
Step 5: the last resort, deleting the pending revision
Some teams fix a stuck release by deleting the Secret for the pending revision. It works, because Helm then sees revision 4 as the latest and the lock is gone.
kubectl get secrets -n shop -l owner=helm,name=web
kubectl delete secret sh.helm.release.v1.web.v5 -n shop
Prefer a rollback anyway. Deleting the record makes Helm forget the attempt but leaves the cluster as the attempt left it. Objects that revision 5 created are no longer in any release manifest, so a later upgrade won't remove them and an uninstall won't either. Use this only when a rollback itself fails, and check afterwards for orphaned objects.
If your Helm uses a different storage backend through the HELM_DRIVER environment variable, the record is a ConfigMap or a database row instead of a Secret, and the same caution applies.
Stop it happening again
Most stuck releases come from a pipeline that kills Helm before Helm's own timeout fires. Helm's --timeout defaults to 5 minutes and limits how long it waits for each Kubernetes operation, such as hooks. Set the CI job timeout comfortably above the Helm timeout. Then Helm reaches its own limit first, records failed and exits cleanly, instead of being killed with the lock held.
Turn on --rollback-on-failure for unattended deploys. In Helm 4 this is the new name for --atomic. When an upgrade fails, Helm rolls back to the last successful release on its own, so a bad chart doesn't leave the release failed and half-applied. Setting it also turns on waiting with the watcher strategy.
helm upgrade web ./web -n shop -f values-prod.yaml \
--rollback-on-failure \
--timeout 10m
Be clear about waiting in Helm 4. Without the --wait flag the strategy is hookOnly, which waits for hooks but not for your workloads to become ready. A bare --wait uses the watcher strategy, which follows resource status, and --wait=legacy keeps the Helm 3 behavior. If the pipeline should only pass when pods are ready, pass --wait or --rollback-on-failure.
Finally, never let two pipelines deploy the same release at once. The pending lock catches most collisions, but the second job still fails, and if someone then kills the first one to unblock it, you have a stuck release again. Serialize deploys per release in your CI system and keep a single owner for each release.