Why Kubernetes Pods and Namespaces Get Stuck Terminating (and When Force-Delete Makes It Worse)
You run kubectl delete pod and get your prompt back. You run kubectl get pods and it is still there, now with STATUS: Terminating. Five minutes later it is still Terminating. An hour later, still Terminating. The same thing happens with namespaces: kubectl delete namespace staging hangs, you Ctrl-C, and the namespace sits in Terminating apparently forever.
The internet's answer is nearly unanimous and nearly always wrong: --grace-period=0 --force for the pod, or patch out all the finalizers for the namespace. Both make the symptom disappear. Both can leave you with orphaned resources that are far harder to find than the stuck object you started with. This is one of the longest-running open issues in Kubernetes precisely because there is no clean force-cleanup button, only manual finalizer surgery that you have to do with your eyes open.
a stuck deletion means a finalizer is waiting on something that has not finished. Find what it is waiting on before you delete anything.
- Pod:
kubectl get pod NAME -o yamland readmetadata.finalizers,deletionTimestamp, and the node's condition. A stuck pod is usually the kubelet unable to unmount a volume or tear down the network. - Namespace:
kubectl get namespace NAME -o yamland readstatus.conditions. It names the exact API group that failed to delete. --forceon a pod whose node is healthy can orphan the container and its mounts. Only force when the node is truly gone.- Stripping namespace finalizers via
/finalizeworks, but only after you have confirmed nothing real is still using the resources.
The signature of a stuck deletion
There is no error string here, which is part of why it is confusing. The evidence is in the object itself. For a pod:
kubectl get pod my-pod -o yaml | grep -A5 -E "deletionTimestamp|finalizers"
metadata:
deletionGracePeriodSeconds: 30
deletionTimestamp: "2026-07-24T01:12:44Z"
finalizers:
- external-attacher/ebs-csi-aws-com
A non-null deletionTimestamp means the API server has accepted the delete. The object is not stuck in the API server; it is waiting for every entry in finalizers to be removed by whatever controller owns it. That is the entire mechanism, and once you see it this way the fog lifts.
Why this happens: deletion is cooperative, not a kill
Deleting an object in Kubernetes is not like rm. It is a two-phase, cooperative protocol:
- You request deletion. The API server sets
deletionTimestampand, for pods, a grace period. The object is now in Terminating but still exists. - Controllers that registered a finalizer do their cleanup, then remove their own finalizer entry.
- When the finalizers list is empty and the grace period has elapsed, the API server actually deletes the object.
So an object stuck in Terminating is stuck at step 2. Some finalizer's controller has not removed itself, either because it is still working, or because it is broken, or because it no longer exists. Naming which of those three it is, is the whole job.
Fixing a stuck pod
Step 1: Read the pod and find the node
kubectl get pod my-pod -o yaml
kubectl get pod my-pod -o wide # note the NODE column
You are looking for two things: which finalizer is present, and whether the node hosting the pod is healthy.
kubectl get node ip-10-0-3-14 -o wide
# Is it Ready? Or NotReady / unreachable?
Step 2: Decide based on the node
This single question decides everything.
| Node state | What is happening | Right move |
|---|---|---|
| Ready | The kubelet is alive and actively failing to finish teardown, usually a stuck volume unmount or CNI cleanup | Fix the underlying cause; do not force yet |
| NotReady / gone | The kubelet cannot report back at all, so the pod can never confirm it stopped | Force-delete is legitimate here |
Step 3a: Node is Ready - find what the kubelet is stuck on
The kubelet logs on that node tell you exactly what it cannot finish:
# on the node, or via your logging stack
journalctl -u kubelet -n 200 --no-pager | grep -i "my-pod"
The two usual culprits are a volume that will not unmount (the CSI driver is unhappy, the backing disk is still attached elsewhere) and a network namespace the CNI cannot tear down. Both leave a finalizer like external-attacher/... in place. Fix that layer, for example detach the stuck EBS volume or restart the CSI node plugin, and the finalizer clears on its own. This is the same discipline as diagnosing a pod with unbound PersistentVolumeClaims: the pod is a symptom, the storage layer is the cause.
Step 3b: Node is gone - force-delete is correct
If the node is genuinely dead, the pod cannot ever confirm its own termination, so force-delete is the right tool, not a hack:
kubectl delete pod my-pod --grace-period=0 --force
Expected output:
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.
pod "my-pod" deleted
Read that warning. It is not boilerplate. On a live node it means a real running container. On a dead node it means nothing, which is why this is safe here and dangerous elsewhere. If the pod belonged to a StatefulSet, also confirm the volume is detached before the replacement pod tries to attach it, or you will trade one stuck pod for another.
Fixing a stuck namespace
A namespace deletion cascades to everything inside it, and it gets stuck when one of those child resources cannot be deleted. The namespace object records exactly which one.
Step 1: Ask the namespace what it is waiting for
kubectl get namespace stuck-ns -o yaml
status:
conditions:
- type: NamespaceDeletionContentFailure
status: "True"
message: 'Failed to delete all resource types, 1 remaining: some.crd.example.com'
phase: Terminating
That message is the answer most guides skip past. It names the API group whose resources could not be removed. Nine times out of ten it is a Custom Resource whose operator has already been uninstalled, so no controller remains to process the deletion.
Step 2: Confirm the controller is really gone and the resource is safe to abandon
kubectl api-resources --api-group=some.crd.example.com
kubectl get some-crd -n stuck-ns -o yaml | grep -A3 finalizers
If the CRD's controller/operator deployment no longer exists, nothing will ever remove that finalizer, and you have to. But first make sure the finalizer was not protecting something external, like a cloud load balancer or a DNS record. If it was, delete that external thing yourself before proceeding, because you are about to make Kubernetes forget it existed.
Step 3: Remove the specific blocking finalizer
Prefer removing the finalizer from the individual stuck resource rather than nuking the namespace's finalizers wholesale:
kubectl patch some-crd my-resource -n stuck-ns \
--type=merge -p '{"metadata":{"finalizers":[]}}'
Once the last blocking child is gone, the namespace finalizes itself within seconds. If the namespace object itself carries a kubernetes finalizer that is stuck, use the documented /finalize subresource, which is the only supported path and requires talking to the raw API:
kubectl proxy &
kubectl get namespace stuck-ns -o json \
| jq '.spec.finalizers = []' \
| curl -sk -H "Content-Type: application/json" -X PUT \
--data-binary @- \
http://127.0.0.1:8001/api/v1/namespaces/stuck-ns/finalize
The /finalize endpoint is what actually clears spec.finalizers; a plain kubectl edit on the namespace will silently refuse to persist that change, which is why so many people conclude the namespace is unkillable.
Verification
# Pod is genuinely gone, not just re-queued
kubectl get pod my-pod
# Error from server (NotFound): pods "my-pod" not found
# Namespace is gone
kubectl get namespace stuck-ns
# Error from server (NotFound): namespaces "stuck-ns" not found
For a force-deleted pod, do the extra check that force-delete skips: on the (now recovered) node, confirm no orphaned container or mount remains.
crictl ps -a | grep my-pod # should be empty
mount | grep my-pod # should be empty
What people get wrong
Reflexive --grace-period=0 --force on a healthy node. This is the single most common mistake. It removes the API object while the container may still be running, its volume still mounted, its IP still allocated. You now have a zombie the control plane no longer knows about, and the next pod that lands there can hit "volume already attached" or a duplicate process. Force is for dead nodes, not for impatience.
Blindly patching all namespace finalizers to []. This makes the namespace vanish and leaks whatever the finalizers were cleaning up. Read the status.conditions message first; it tells you the one thing actually blocking you, so you can deal with that specifically instead of carpet-bombing.
Restarting the whole cluster or the API server. It changes nothing, because the finalizer is still there when everything comes back. You have added an outage to your stuck object.
Assuming Terminating means broken. A pod draining a long-running connection, or a namespace with a large number of resources, can legitimately sit in Terminating for a while. Give it a minute and read the object before you intervene. Intervening early is how you create the orphans.
When it is still broken
- The finalizer clears but the object comes back. A controller is recreating it. Scale down or delete the owning Deployment/Operator first, then delete the object.
- Namespace still stuck after clearing the named resource. Re-read
status.conditions; there may be a second group. It fails on them one at a time. - You are on a managed cluster and cannot reach the kubelet. Use the provider's node console or drain/replace the node. On EKS specifically, a stuck pod can also be an identity problem, not a lifecycle one; see kubectl Unauthorized on EKS if your commands themselves are failing.
- Everything is Terminating cluster-wide. That is not this problem. Check the API server and etcd health, and whether an admission webhook is timing out and blocking all deletes.
The rule to carry away: Terminating is a state that is waiting, and the object always records what it is waiting for. Force-delete does not answer the question, it just stops you from having to ask it, and the answer is where the real cleanup lives.
Frequently asked questions
- How do I force delete a pod stuck in Terminating?
- kubectl delete pod NAME --grace-period=0 --force removes the object from the API immediately, but it does not confirm the container actually stopped. Only use it after checking the node's kubelet, because on a healthy node it can leave a running container, an unreleased volume or a dangling network namespace behind. If the node is genuinely gone (NotReady, powered off), force-delete is the correct move.
- Why is my namespace stuck in Terminating forever?
- Something inside it still has a finalizer that cannot complete, usually a custom resource whose controller has already been uninstalled, so nothing is left to remove the finalizer. GET the namespace status subresource to see exactly which API group failed to delete, then remove that specific blocker rather than blindly stripping all finalizers.
- Is it safe to remove finalizers from a namespace?
- Only after you confirm nothing is still using the resources the finalizer protects. Patching finalizers to an empty list via the /finalize subresource forces the namespace to disappear, but any orphaned cloud load balancers, disks or external records the finalizer was meant to clean up are simply leaked. It is a last resort, not a first move.
- What does deletionTimestamp mean on a resource?
- It is the moment the API server marked the object for deletion. Once it is set, the object stays visible in Terminating until every finalizer in metadata.finalizers is removed by its controller. A non-null deletionTimestamp plus a non-empty finalizers list is the exact signature of a stuck deletion, and it tells you the object is waiting on a controller, not on the API server.