| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Bash-compatible Linux or macOS shell with kubectl configured; Kubernetes cluster with Linux worker nodes |
| Cert prep | CKA · CKAD · CKS |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm |
| Privilege | Normal user (no sudo required on the workstation) |
| Scope | Normal Pod termination sequence, confirming stuck Terminating, graceful delete, shorter grace period, terminationGracePeriodSeconds and preStop routing, unreachable node signals, finalizer inspection and cautious removal, force delete with warnings, controller replacement verification, and cause-to-fix tables. Does not cover node recovery, CSI detach depth, StatefulSet split-brain recovery, finalizer controller internals, namespace stuck in Terminating, control-plane force deletion, or full Pod lifecycle teaching covered in the Pod lifecycle guide. |
| Related guides | kubectl logs, Events and describe Deployments and rolling updates Fix Pending and ContainerCreating Pods |
Terminating—after you issued kubectl delete and the row does not clear. It does not cover namespaces stuck in Terminating, node drain and recovery, or Pods that never started (Pending, ContainerCreating). For STATUS routing before deletion, use the Kubernetes Pod troubleshooting workflow.
A few seconds of Terminating is normal graceful shutdown. Minutes with a deletion timestamp usually mean the kubelet, a finalizer, or volume detach is still in progress. This walkthrough runs deletes in terminating-lab, inspects what blocks removal, and applies force delete only when graceful paths cannot finish.
Quick reference
Run these steps on the stuck Pod before you force delete. Replace POD and NS with your Pod name and namespace.
| Step | Command | What to check |
|---|---|---|
| 1 | kubectl get pod POD -n NS -o wide |
Confirm STATUS is Terminating and note the assigned NODE |
| 2 | kubectl get pod POD -n NS -o jsonpath='deletionTimestamp={.metadata.deletionTimestamp}{"\n"}deletionGrace={.metadata.deletionGracePeriodSeconds}{"\n"}' |
Compare elapsed time to the active grace period — brief Terminating may still be normal |
| 3 | kubectl describe pod POD -n NS and kubectl get pod POD -n NS -o jsonpath='{.metadata.finalizers}{"\n"}' |
Events, finalizers, and volume or node warnings |
| 4 | kubectl get node NODE |
NotReady or unreachable nodes can delay kubelet confirmation |
| 5 | kubectl delete pod POD -n NS --grace-period=10 |
Shorter graceful window when preStop or spec grace is too long — see graceful shutdown delays |
| 6 | kubectl patch pod POD -n NS --type=json -p='[{"op": "remove", "path": "/metadata/finalizers"}]' |
Only when you confirmed the finalizer owner cannot finish — see blocking finalizers |
| 7 | kubectl delete pod POD -n NS --grace-period=0 --force --wait=false |
Last resort when graceful paths cannot complete — see force delete |
How Kubernetes Terminates a Pod
When you delete a Pod, Kubernetes does not remove the API object instantly. The normal sequence:
- API server records
deletionTimestampand the active grace period - Kubelet starts local shutdown
- A selected Service endpoint is marked terminating and
ready=false; it is not necessarily removed from its EndpointSlice immediately - Kubelet runs
preStopwhen configured and grace is non-zero - The runtime sends the configured stop signal, image
STOPSIGNAL, or runtime default—commonlySIGTERM - Any remaining process is killed when the grace budget expires
- The API object disappears after local cleanup completes and its finalizer list is empty
Terminating is a kubectl display status based on the deletion timestamp, not a Pod phase. Terminating EndpointSlice entries normally have ready=false; Services using publishNotReadyAddresses: true are an exception.
Brief Terminating in kubectl get is expected during graceful shutdown. The troubleshooting starts when STATUS stays Terminating long after the grace period should have ended.
Terminating status and deletion metadata
A non-empty deletionTimestamp means deletion is in progress. Compare elapsed time to the active grace period recorded in metadata, not only the Pod spec default.
Grace period, preStop, stop signal, and EndpointSlices
spec.terminationGracePeriodSeconds sets the Pod's default graceful-shutdown budget (30 seconds when omitted). The grace-period countdown begins before preStop. The hook and the application's response to the stop signal share the same budget. If preStop is still running when the period expires, kubelet requests a one-time two-second extension.
A long preStop script that sleeps 60 seconds needs a grace period longer than 60—not a force delete on day one. Hook behaviour and the full shutdown sequence are in Kubernetes Pods and Pod lifecycle.
Prepare the Terminating Pod Lab
Create the namespace for delete experiments:
kubectl create namespace terminating-labThe sections below create and delete Pods inside that namespace.
Confirm Whether Deletion Is Actually Stuck
Inspect deletion timestamp and active grace period
Create a Pod with a deliberately long preStop hook so it remains observable during deletion:
kubectl apply -f - <<'YAML'
apiVersion: v1
kind: Pod
metadata:
name: inspect-pod
namespace: terminating-lab
spec:
terminationGracePeriodSeconds: 90
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
lifecycle:
preStop:
exec:
command: ["sh", "-c", "echo preStop-start; sleep 60"]
YAMLkubectl wait pod/inspect-pod -n terminating-lab --for=condition=Ready --timeout=120sRequest deletion:
kubectl delete pod inspect-pod -n terminating-lab --wait=falseThis guarantees enough time to inspect the deleting object.
Check STATUS, age, and node while shutdown runs:
kubectl get pod inspect-pod -n terminating-lab -o wideRead the active deletion grace period:
kubectl get pod inspect-pod -n terminating-lab -o jsonpath='deletionTimestamp={.metadata.deletionTimestamp}{"\n"}deletionGrace={.metadata.deletionGracePeriodSeconds}{"\n"}specGrace={.spec.terminationGracePeriodSeconds}{"\n"}'Sample output:
deletionTimestamp=2026-07-26T14:06:21Z
deletionGrace=90
specGrace=90metadata.deletionGracePeriodSeconds records the active deletion grace separately from spec.terminationGracePeriodSeconds. A later delete with --grace-period=10 should show deletionGrace=10 while specGrace can still show 30.
Check Events, finalizers, and node reachability
Read events and finalizers:
kubectl describe pod inspect-pod -n terminating-lab | grep -A10 '^Events:'kubectl get pod inspect-pod -n terminating-lab -o jsonpath='{.metadata.finalizers}{"\n"}'Empty finalizers and no warning events usually indicate normal shutdown only when the active deletion grace period has not expired and the assigned node remains reachable. If the object remains beyond that period, inspect node reachability and kubelet communication.
A node that cannot reach the API server can leave deletion pending significantly longer than the requested grace period.
Check the Pod node and node conditions when shutdown stalls:
kubectl get pod <pod-name> -n <namespace> -o widekubectl get node <node-name>kubectl describe node <node-name> | grep -A6 'Conditions:'Node recovery and repair are outside this article's scope.
Resolve Graceful Shutdown Delays
Delete normally
Create a standalone Pod and request standard graceful deletion:
kubectl run normal-pod -n terminating-lab --image=busybox:1.36 --restart=Never --command -- sleep 3600kubectl wait pod/normal-pod -n terminating-lab --for=condition=Ready --timeout=120skubectl delete pod normal-pod -n terminating-labSample output:
pod "normal-pod" deleted from terminating-lab namespaceA bare Pod like normal-pod is gone after shutdown completes—it is not replaced. Controller-owned Pods (Deployment, StatefulSet, DaemonSet) are recreated when the controller notices the missing replica.
Shorten the grace period
Create a Pod with a controlled shutdown and request a shorter grace window:
kubectl apply -f - <<'YAML'
apiVersion: v1
kind: Pod
metadata:
name: grace-pod
namespace: terminating-lab
spec:
terminationGracePeriodSeconds: 30
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 20"]
YAMLkubectl wait pod/grace-pod -n terminating-lab --for=condition=Ready --timeout=120skubectl delete pod grace-pod -n terminating-lab --grace-period=10 --wait=falseInspect the actual override:
kubectl get pod grace-pod -n terminating-lab -o jsonpath='deletionGrace={.metadata.deletionGracePeriodSeconds}{"\n"}'Sample output:
deletionGrace=10Then wait for deletion:
kubectl wait pod/grace-pod -n terminating-lab --for=delete --timeout=30s--grace-period=10 sets the maximum graceful-shutdown budget to ten seconds. It does not make Kubernetes wait ten seconds when the process exits earlier. kubectl wait --for=delete is supported specifically for confirming object removal.
Use this when the default 30-second window is unnecessarily long, not as a substitute for fixing finalizers or node issues.
Fix long preStop hooks and signal handling
When applications ignore SIGTERM or preStop consumes the entire budget, fix signal handling or increase terminationGracePeriodSeconds before force deleting. A grace period is a maximum shutdown budget, not a mandatory delay.
Resolve Blocking Finalizers
Finalizers are metadata keys that block object removal until a controller finishes cleanup.
Identify the responsible controller
Create a Pod with a blocking finalizer for demonstration:
kubectl apply -f - <<'YAML'
apiVersion: v1
kind: Pod
metadata:
name: finalizer-pod
namespace: terminating-lab
finalizers:
- example.com/block-delete
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
YAMLkubectl wait pod/finalizer-pod -n terminating-lab --for=condition=Ready --timeout=120sDelete it and wait until the timestamp exists:
kubectl delete pod finalizer-pod -n terminating-lab --wait=falsekubectl wait pod/finalizer-pod -n terminating-lab --for=jsonpath='{.metadata.deletionTimestamp}' --timeout=30sInspect:
kubectl get pod finalizer-pod -n terminating-lab -o jsonpath='deletionTimestamp={.metadata.deletionTimestamp}{" finalizers="}{.metadata.finalizers}{"\n"}'Sample output:
deletionTimestamp=2026-07-26T14:05:38Z finalizers=["example.com/block-delete"]The container may already have stopped while the finalizer continues holding the API object. example.com/block-delete is intentionally a dead custom finalizer: no controller owns it, so it never clears automatically.
Remove a dead finalizer only when safe
Do not remove finalizers as the first step. Confirm no controller will complete the cleanup—broken operator, orphaned object, or lab resource.
Finalizers keep an object present after its deletion timestamp is set until every finalizer is removed. Manual removal should happen only after understanding and otherwise completing the intended cleanup.
Remove finalizers with a JSON patch:
kubectl patch pod finalizer-pod -n terminating-lab --type=json -p='[{"op": "remove", "path": "/metadata/finalizers"}]'Sample output:
pod/finalizer-pod patchedVerify deletion:
kubectl wait pod/finalizer-pod -n terminating-lab --for=delete --timeout=30sAlternatively, kubectl edit pod finalizer-pod -n terminating-lab and delete the finalizers list entries manually.
Force Delete a Pod
When graceful deletion cannot complete and you accept API-level removal without kubelet confirmation:
kubectl run force-pod -n terminating-lab --image=busybox:1.36 --restart=Never --command -- sleep 3600kubectl wait pod/force-pod -n terminating-lab --for=condition=Ready --timeout=120skubectl delete pod force-pod -n terminating-lab --grace-period=0 --force --wait=falseSample 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 "force-pod" force deleted from terminating-lab namespaceWhat force deletion changes
--grace-period=0removes the graceful-shutdown delay, while--forceallows deletion without kubelet confirmation. Finalizers can still prevent final removal.- A controller may create a replacement Pod immediately if one owned the deleted Pod
- On an unreachable node, the old process may still run until the runtime or node recovery stops it
--grace-period=0 --force does not make an unknown finalizer safe to remove; finalizers must still be investigated separately.
Stateful and duplicate-process risks
Force deletion removes the API object without waiting for confirmation that its processes stopped. This can allow duplicate processes with the same logical identity and cause data inconsistency.
--force.
Verify the object is gone:
kubectl get pod force-pod -n terminating-labSample output:
Error from server (NotFound): pods "force-pod" not foundVerify Standalone and Controller-Owned Deletion
For a standalone Pod, confirm the name no longer exists. For controller-owned Pods, confirm a new instance with a new UID:
kubectl create deployment web -n terminating-lab --image=nginx:1.27.0 --replicas=1kubectl rollout status deployment/web -n terminating-lab --timeout=120sOLD_POD=$(kubectl get pods -n terminating-lab -l app=web -o jsonpath='{.items[0].metadata.name}')OLD_UID=$(kubectl get pod "$OLD_POD" -n terminating-lab -o jsonpath='{.metadata.uid}')Delete and wait until the original object is gone:
kubectl delete pod "$OLD_POD" -n terminating-labWait for the replacement:
kubectl wait pod -n terminating-lab -l app=web --for=condition=Ready --timeout=120sNEW_POD=$(kubectl get pods -n terminating-lab -l app=web -o jsonpath='{.items[0].metadata.name}')NEW_UID=$(kubectl get pod "$NEW_POD" -n terminating-lab -o jsonpath='{.metadata.uid}')printf 'old=%s %s\nnew=%s %s\n' "$OLD_POD" "$OLD_UID" "$NEW_POD" "$NEW_UID"This proves the replacement has both a new name and UID rather than relying only on a representative Pod row. Replica count should match the Deployment spec.
Troubleshoot Common Terminating Pod Causes
| Symptom or cause | Likely cause | Fix |
|---|---|---|
Terminating under 60s, no finalizers |
Normal graceful shutdown | Wait; check active deletionGracePeriodSeconds and preStop |
| Application still shutting down | Grace budget in use | Wait for grace period; read preStop duration |
Application ignores SIGTERM |
Process does not exit on signal | Fix signal handling; extend grace period or force when safe |
Long preStop hook |
Hook consumes grace budget | Increase terminationGracePeriodSeconds |
Terminating minutes, finalizer present |
Controller cleanup pending | Fix owner or carefully remove dead finalizer |
Terminating, node NotReady |
Kubelet cannot confirm stop | Restore node or force delete with split-brain risk |
| Unreachable node | API waits longer than grace period | Check node reachability; node recovery or force delete when appropriate |
| Delete returns immediately, Pod reappears | Deployment/StatefulSet replacement | Expected—edit controller template if unwanted |
| Blocking finalizer | Cleanup owner missing or stuck | Identify owner; remove only if cleanup cannot complete |
| Volume detach delayed | Storage still attached | Read volume events; route storage detach separately |
| Force delete warning in output | API object removed before kubelet confirms | Verify process on node if node was reachable |
What's Next
- Fix a Kubernetes Deployment Not Creating Pods
- Fix Kubernetes Node NotReady and kubelet Errors
- Troubleshoot Kubernetes Control Plane Components
References
- Pod termination — grace period, preStop, and endpoint updates
- Force delete workload resources — official force-delete guidance and risks
- Finalizers — how deletion blocking works
Summary
Terminating is normal for a short window while the kubelet runs preStop, sends SIGTERM, and waits within the active grace budget. Stuck deletion means the API recorded deletionTimestamp but something still blocks removal—slow shutdown, a finalizer, volume detach, or an unreachable node.
In terminating-lab, graceful delete cleared normal-pod within the grace window, a custom finalizer held finalizer-pod until patched away, and a Deployment replaced deleted Pods with new names and UIDs. Use shorter --grace-period when shutdown should be faster, not as the first response to every stall.
Reserve --grace-period=0 --force for cases where the API object must clear and you accept that the kubelet might not have stopped the process—especially on unreachable nodes. For Pods that fail before they run, use Pending and ContainerCreating troubleshooting instead of force delete.

