
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.
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:
deletionTimestampempty. The pod is not terminating at all. Yourdeletenever reached the API server.deletionGracePeriodSecondsstill counting down against the timestamp. Wait. This is normal shutdown, not a stuck pod.finalizersnon-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--forcedoes 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 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
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:
| Cause | What --force does |
|---|---|
| Grace period running | Skips the wait. Your container gets SIGKILL with no chance to flush |
| Node unreachable | The correct fix, after confirming the node is dead |
| Finalizer | Nothing. The object remains |
| Volume detaching | Removes 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
terminationGracePeriodSecondsdeliberately. 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 KubeGlanceThe 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

