# How to fix a Helm release stuck in pending-upgrade

> Why Helm says another operation is in progress, how to recover a release stuck in pending-upgrade or pending-install, and how to stop it happening again.

Published: 2026-11-05
Author: k8wiz Team
URL: https://k8wiz.com/articles/fix-a-helm-release-stuck-in-pending-upgrade

Sooner or later a deploy pipeline fails with this error, and every retry fails the same way.

```text
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.

```bash
helm status web -n shop
helm history web -n shop
```

```text
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.

```bash
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.

```bash
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.

```bash
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`.

```text
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.

```bash
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.

```bash
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.

```bash
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.

```bash
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.
