KubeGlanceDownload
Pod stuck in Terminating: what is actually blocking the delete

troubleshooting

Pod stuck in Terminating: what is actually blocking the delete

A pod stuck in Terminating is one of four things: a grace period, an unreachable node, a finalizer, or a volume that will not detach. How to tell which.

· 11 min read

Terminating is not a phase. It is what kubectl prints for any pod whose metadata.deletionTimestamp is set and which still exists. The pod object is waiting for something, and there are only four candidates: the grace period has not elapsed, the kubelet that owns the pod cannot be reached, a finalizer is still attached, or a volume will not detach.

Each one has a different fix, and only one of them is fixed by --force.

The fastest check

kubectl get pod <pod> -o jsonpath='{.metadata.deletionTimestamp}{"\t"}{.metadata.deletionGracePeriodSeconds}{"\t"}{.metadata.finalizers}{"\n"}'

On a pod held by a finalizer, that prints something like:

2026-08-25T11:06:20Z	30	["example.com/blocks-deletion"]

Three fields, and they eliminate three of the four causes between them:

  • deletionTimestamp empty. The pod is not terminating at all. Your delete never reached the API server.
  • deletionGracePeriodSeconds still counting down against the timestamp. Wait. This is normal shutdown, not a stuck pod.
  • finalizers non-empty. Something has claimed the right to run before this object may be removed, and until it withdraws that claim the API server will not delete the record. This is the case --force does not solve.

If all three look fine and the pod is still there, the node is the problem.

What deletion actually does

Deleting a pod is not one operation. It is a request to the API server, which sets a timestamp, after which two independent parties have to finish their work: the kubelet that runs the containers, and every controller holding a finalizer.

The API server never deletes the record itself until the grace period has elapsed and the finalizer list is empty.

The important property is that the API server is a bookkeeper here, not an executor. It marks the object and waits. Nothing in that sequence has a timeout that eventually gives up — a finalizer whose controller is gone will hold the object indefinitely, and that is by design.

1. The grace period has not elapsed

The default is 30 seconds, and it starts when the deletion is accepted, not when the process exits. The kubelet sends SIGTERM, waits, then sends SIGKILL.

kubectl get pod <pod> -o jsonpath='{.spec.terminationGracePeriodSeconds}{"\n"}'

A pod that ignores SIGTERM will always take the full grace period. If yours is set to 3600 for a batch job, a “stuck” pod is a pod behaving exactly as configured. Check this before anything else — it is the most common false alarm.

2. The node is gone

If the kubelet cannot report back, nobody can confirm the containers stopped, and the API server will not remove a pod record while it still believes containers may be running. This is the failure mode people hit after a node crashes.

kubectl get pod <pod> -o wide
kubectl get node <node>

A node in NotReady with a pod in Terminating on it is this case. Kubernetes will not resolve it on its own for pods without a controller, because it cannot distinguish “node is dead” from “node is briefly unreachable and still running your container”. Deleting the pod record while the container is genuinely still running is precisely what causes two instances of a StatefulSet replica to write to the same volume.

This is the one case --force --grace-period=0 is for. It tells the API server to stop waiting for confirmation. Read the warning it prints, because it is accurate:

Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.

Do not do this to a StatefulSet pod on a node you have not confirmed is dead. The whole guarantee a StatefulSet offers is at-most-one semantics per ordinal, and force-deleting the record is you asserting the container is gone. If the node comes back, the replacement pod and the original will both be running, both attached to the same identity, and you have to fix the data by hand. Fence or confirm the node first, then force.

3. A finalizer is holding it

A finalizer is a string in metadata.finalizers. It means “do not remove this object until I say so”. Something has to remove that string, and if the controller that added it has been uninstalled, nothing ever will.

Verified on 1.36: --force --grace-period=0 does not clear finalizers. Running it against a pod with one attached, the command reports the pod force deleted and the object is still there afterwards — the following patch succeeds against it:

kubectl patch pod <pod> -p '{"metadata":{"finalizers":null}}' --type=merge
pod/finalized patched

Once the list is empty the object disappears immediately, because the grace period had already expired.

Before you clear it, look at what the finalizer is named after. It is usually the controller that owns some external resource — a cloud load balancer, a volume, a certificate. Removing the string tells Kubernetes to forget the object, which does nothing about the resource the controller was going to clean up. That resource becomes an orphan you will pay for or trip over later. Clearing a finalizer is a legitimate last step; it is not a diagnosis.

4. A volume will not detach

The pod’s containers stopped, but the volume is still attached to a node and the attach/detach controller is retrying. The pod stays until it is released.

kubectl describe pod <pod> | grep -A5 Events
kubectl get volumeattachment

The events name the volume. In practice this is either a CSI driver that is not running on the node, or a cloud provider that thinks the disk is still mounted elsewhere. It resolves on its own once the detach succeeds, and force-deleting the pod does not speed up the detach — it just removes your visibility into it.

Which one is it

Four causes, and only one of them is fixed by force delete.

The --force habit is the actual problem

kubectl delete pod --force --grace-period=0 works often enough that it becomes reflex, and each of the four causes it appears to fix, it fixes differently:

CauseWhat --force does
Grace period runningSkips the wait. Your container gets SIGKILL with no chance to flush
Node unreachableThe correct fix, after confirming the node is dead
FinalizerNothing. The object remains
Volume detachingRemoves the pod record, leaves the detach in progress

Two of those four are silent damage. If it is your default, you are trading a diagnosis for a disappearing symptom.

Preventing it

  • Handle SIGTERM. A process that exits on the signal terminates in milliseconds. One that ignores it costs you the full grace period every time, on every deploy.
  • Set terminationGracePeriodSeconds deliberately. The default 30 is fine for a web service and far too short for something draining a queue. Make it a decision rather than an inherited number.
  • Audit finalizers you did not add. kubectl get pods -o json | jq '.items[] | select(.metadata.finalizers) | .metadata.name' after uninstalling an operator is a cheap check, and uninstalled operators are where orphan finalizers come from.
  • Never force-delete a StatefulSet pod as a habit. If it is routine, the real problem is the shutdown path, and you are papering over it.

Where the pod never terminated because it never started properly, the symptom usually points elsewhere: a container that will not stay up is covered in how to fix CrashLoopBackOff, and one that was killed for memory in OOMKilled and exit code 137. If kubectl delete is not reaching the API server at all, start with kubectl connection and TLS errors.

Deletion timestamps and finalizers are two taps away, not two jsonpath expressions. KubeGlance is a native Kubernetes client for iPhone and iPad, with a full Mac app on the same core — pod detail, events and workload state straight from your kubeconfig.

Get KubeGlance
#terminating #finalizers #pods #kubectl #troubleshooting

The Kubernetes dashboard that fits in your pocket

KubeGlance is a native Kubernetes client for iPhone and iPad — the real dashboard, not a companion — with a full Mac app on the same core. Pods, workloads, logs and events, straight from your kubeconfig.

Download KubeGlance