Back to blog

Pods Stuck in Terminating: A Production Decision Tree for Finalizers, Volumes, and Dead Nodes

A Pod stuck in Terminating is rarely “just an annoyance”. In production it can:

  • block rollouts (maxSurge/maxUnavailable budgets get consumed),
  • exhaust quotas (CPU/memory, IPs, PVC attachments),
  • keep stateful identities “occupied” (worst case: split brain),
  • hide bigger cluster problems (dead node, CSI issues, stuck kubelet cleanup).

This runbook is intentionally conservative: avoid data loss first, then unstick.

Tested on: Kubernetes 1.27–1.30 (managed + self-managed), containerd, and common CSI drivers. Some commands require cluster-admin RBAC.

What “Terminating” really means (the part that matters)

When you delete an object that has finalizers, Kubernetes sets a .metadata.deletionTimestamp, returns quickly, and the object stays around until controllers remove the finalizers after cleanup. If the cleanup can’t complete, the object stays “terminating” indefinitely.

So the question isn’t “how do I delete it”, but what cleanup is blocked.

Quick triage: is it safe to force-delete?

Before you touch --force, answer these:

Usually safe-ish (still validate)

  • Stateless Deployment replica, no local disk state, no exclusive external locks.
  • You can confirm the container is not still serving traffic (or can’t serve any).

High risk (treat as an incident)

  • StatefulSet member (stable identity + stable storage).
  • Anything with quorum semantics (databases, Kafka, etcd, RabbitMQ clusters).
  • Pods on a node that might still be alive (network partition scenario).

If it’s a StatefulSet, forcing deletion can free the name in the API and allow a replacement to be created even if the old Pod is still running. That’s how you create “at most one” violations.

Collect the minimum evidence (don’t guess)

Set variables:

NS=default
POD=my-pod-abc123

1) Snapshot the Pod and the node

kubectl get pod -n "$NS" "$POD" -o wide
kubectl describe pod -n "$NS" "$POD"

2) Check deletion timestamp + finalizers (fast signal)

kubectl get pod -n "$NS" "$POD" -o jsonpath='{.metadata.deletionTimestamp}{"\n"}{.metadata.finalizers}{"\n"}'

If you see finalizers, you’re in the “finalizer branch” below.

3) Look at recent events in time order

kubectl get events -n "$NS" --sort-by=.lastTimestamp | tail -n 60

For volume-related hangs you’ll often see FailedMount, FailedAttachVolume, UnmountVolume, Device or resource busy, etc.

The decision tree (copy/paste into your runbooks)

Step 1 — Is the node reachable?

Find the node:

NODE="$(kubectl get pod -n "$NS" "$POD" -o jsonpath='{.spec.nodeName}')"
kubectl get node "$NODE" -o wide
kubectl describe node "$NODE"

If the node is NotReady / Unreachable:

  • Treat this as a node incident first.
  • If it’s a transient partition, wait or fix node connectivity.
  • If the node is confirmed dead forever, replacing/removing the node is often the cleanest path (it allows the control plane to move on).

If you force-delete a Pod on an unreachable node, you’re asserting it will never come back and run again. Don’t do that lightly.

Step 2 — Are finalizers blocking deletion?

If finalizers are present, list them:

kubectl get pod -n "$NS" "$POD" -o jsonpath='{range .metadata.finalizers[*]}{.}{"\n"}{end}'

Now classify the finalizer:

These can indicate a volume is still considered “in use”. If the Pod has PVCs, list them:

kubectl get pod -n "$NS" "$POD" -o jsonpath='{range .spec.volumes[*]}{.persistentVolumeClaim.claimName}{"\n"}{end}'
kubectl get pvc -n "$NS"
kubectl get pv

If you have permissions, inspect VolumeAttachments (CSI):

kubectl get volumeattachments

If you can’t list VolumeAttachments (RBAC), rely on events + CSI controller logs (cluster-dependent).

B) Custom controller finalizers (example: example.com/...)

This usually means the controller that owns the finalizer is:

  • down,
  • stuck,
  • or blocked by an external dependency (cloud API, webhook, etc.).

Fix is usually:

  • restore the controller,
  • make cleanup succeed,
  • let it remove the finalizer automatically.

Only after you understand the impact should you remove finalizers manually.

Step 3 — Volume / CSI cleanup stalls (the most common real root cause)

If events show volume issues, focus here:

  • Is detach/attach stuck?
  • Is kubelet unable to unmount because a process still holds the mount?
  • Is the node filesystem unhealthy?

If you have node access, the fastest confirmation is usually kubelet logs + mount table:

journalctl -u kubelet --since "30 min ago" --no-pager | tail -n 200
mount | grep -E "kubelet/pods|plugins/kubernetes.io|plugins/kubernetes.io/csi" || true

If you don’t have node access (managed clusters), your best bet is:

  • events,
  • CSI controller logs (if accessible),
  • and node replacement if the node is unhealthy.

Escalation: force deletion (last resort, do it deliberately)

1) Force delete the API object

kubectl delete pod -n "$NS" "$POD" --grace-period=0 --force

This removes the API object immediately, but it does not guarantee the process is dead.

2) Nuclear option: remove finalizers

Only do this when you understand the consequences (resource leaks / broken invariants):

kubectl patch pod -n "$NS" "$POD" -p '{"metadata":{"finalizers":null}}' --type=merge

Prevention: make Terminating boring

Guardrails

  • Alert on Pods stuck in Terminating longer than X minutes (pick X based on your normal grace periods).
  • Alert on frequent volume attach/detach failures and FailedMount events.
  • Alert on Nodes stuck NotReady / Unreachable longer than expected.

Engineering hygiene

  • Treat finalizers as production code: add timeouts, retries with backoff, and “cleanup failed” visibility.
  • For Stateful workloads, document a safe force-delete policy (who can do it, when, and how to ensure the old member is truly dead).
  • Rehearse node loss and CSI failure scenarios in staging.

What I’d do in prod

If I’m on call and Pods are piling up in Terminating, my default sequence is:

  1. Confirm node health and whether the kubelet is reachable.
  2. Inspect finalizers and classify them (system/storage vs custom controller).
  3. If volumes are involved, treat it as a CSI / node cleanup problem, not a “kubectl problem”.
  4. Only when I can justify it, force-delete (and I do it with explicit acknowledgement of the risk, especially for StatefulSets).

FAQ

Why does kubectl delete pod return quickly but the pod stays Terminating?

Because deletion becomes asynchronous when finalizers exist: the API marks it for deletion and waits for controllers to remove finalizers.

Is kubectl delete --force --grace-period=0 guaranteed to kill the container?

No. It removes the object from the API immediately. The process may still be running on the node if kubelet/container runtime is unhealthy.

When is it safe to patch finalizers to null?

Only when you accept the consequences (leaked resources / broken invariants) and you’ve concluded the controller cleanup will not happen.

Why is force-deleting StatefulSet pods dangerous?

It can violate “at most one” semantics: a replacement Pod can be created while the old one might still be running and communicating.

  • /en/blog/kubernetes-graceful-shutdown-rollouts/ (termination semantics and rollout error bursts)
  • /en/blog/kubernetes-inode-exhaustion-overlayfs/ (node filesystem failures that look “random”)
  • /en/blog/etcd-compaction-quota-alarm/ (control-plane symptoms that often accompany bigger incidents)

Further reading

Related posts

Cite this article

If you reference this post, please link to the original URL and credit the author.

Michal Drozd. "Pods Stuck in Terminating: A Production Decision Tree for Finalizers, Volumes, and Dead Nodes". https://www.michal-drozd.com/en/blog/kubernetes-pod-stuck-terminating-playbook/ (Published November 26, 2025).