Delete a Stuck or Terminating Kubernetes Pod

Delete a Kubernetes Pod stuck in Terminating by checking grace periods, preStop hooks, finalizers, node reachability, and force-deletion risks.

Published

Updated

Read time 11 min read

Reviewed byDeepak Prasad

Delete Kubernetes Pod stuck in Terminating with grace period finalizers and force delete
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
IMPORTANT
This guide removes Pod objects stuck in 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:

  1. API server records deletionTimestamp and the active grace period
  2. Kubelet starts local shutdown
  3. A selected Service endpoint is marked terminating and ready=false; it is not necessarily removed from its EndpointSlice immediately
  4. Kubelet runs preStop when configured and grace is non-zero
  5. The runtime sends the configured stop signal, image STOPSIGNAL, or runtime default—commonly SIGTERM
  6. Any remaining process is killed when the grace budget expires
  7. 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:

bash
kubectl create namespace terminating-lab

The 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:

bash
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"]
YAML
bash
kubectl wait pod/inspect-pod -n terminating-lab --for=condition=Ready --timeout=120s

Request deletion:

bash
kubectl delete pod inspect-pod -n terminating-lab --wait=false

This guarantees enough time to inspect the deleting object.

Check STATUS, age, and node while shutdown runs:

bash
kubectl get pod inspect-pod -n terminating-lab -o wide

Read the active deletion grace period:

bash
kubectl get pod inspect-pod -n terminating-lab -o jsonpath='deletionTimestamp={.metadata.deletionTimestamp}{"\n"}deletionGrace={.metadata.deletionGracePeriodSeconds}{"\n"}specGrace={.spec.terminationGracePeriodSeconds}{"\n"}'

Sample output:

output
deletionTimestamp=2026-07-26T14:06:21Z
deletionGrace=90
specGrace=90

metadata.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:

bash
kubectl describe pod inspect-pod -n terminating-lab | grep -A10 '^Events:'
bash
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:

bash
kubectl get pod <pod-name> -n <namespace> -o wide
bash
kubectl get node <node-name>
bash
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:

bash
kubectl run normal-pod -n terminating-lab --image=busybox:1.36 --restart=Never --command -- sleep 3600
bash
kubectl wait pod/normal-pod -n terminating-lab --for=condition=Ready --timeout=120s
bash
kubectl delete pod normal-pod -n terminating-lab

Sample output:

output
pod "normal-pod" deleted from terminating-lab namespace

A 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:

bash
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"]
YAML
bash
kubectl wait pod/grace-pod -n terminating-lab --for=condition=Ready --timeout=120s
bash
kubectl delete pod grace-pod -n terminating-lab --grace-period=10 --wait=false

Inspect the actual override:

bash
kubectl get pod grace-pod -n terminating-lab -o jsonpath='deletionGrace={.metadata.deletionGracePeriodSeconds}{"\n"}'

Sample output:

output
deletionGrace=10

Then wait for deletion:

bash
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:

bash
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"]
YAML
bash
kubectl wait pod/finalizer-pod -n terminating-lab --for=condition=Ready --timeout=120s

Delete it and wait until the timestamp exists:

bash
kubectl delete pod finalizer-pod -n terminating-lab --wait=false
bash
kubectl wait pod/finalizer-pod -n terminating-lab --for=jsonpath='{.metadata.deletionTimestamp}' --timeout=30s

Inspect:

bash
kubectl get pod finalizer-pod -n terminating-lab -o jsonpath='deletionTimestamp={.metadata.deletionTimestamp}{" finalizers="}{.metadata.finalizers}{"\n"}'

Sample output:

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:

bash
kubectl patch pod finalizer-pod -n terminating-lab --type=json -p='[{"op": "remove", "path": "/metadata/finalizers"}]'

Sample output:

output
pod/finalizer-pod patched

Verify deletion:

bash
kubectl wait pod/finalizer-pod -n terminating-lab --for=delete --timeout=30s

Alternatively, 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:

bash
kubectl run force-pod -n terminating-lab --image=busybox:1.36 --restart=Never --command -- sleep 3600
bash
kubectl wait pod/force-pod -n terminating-lab --for=condition=Ready --timeout=120s
bash
kubectl delete pod force-pod -n terminating-lab --grace-period=0 --force --wait=false

Sample output:

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 namespace

What force deletion changes

  • --grace-period=0 removes the graceful-shutdown delay, while --force allows 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.

IMPORTANT
Force delete is risky for workloads that must never have two active instances—StatefulSets with shared storage, leader-elected apps, or Pods with identity tied to one process. Prefer fixing finalizers, node reachability, or volume detach before --force.

Verify the object is gone:

bash
kubectl get pod force-pod -n terminating-lab

Sample output:

output
Error from server (NotFound): pods "force-pod" not found

Verify 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:

bash
kubectl create deployment web -n terminating-lab --image=nginx:1.27.0 --replicas=1
bash
kubectl rollout status deployment/web -n terminating-lab --timeout=120s
bash
OLD_POD=$(kubectl get pods -n terminating-lab -l app=web -o jsonpath='{.items[0].metadata.name}')
bash
OLD_UID=$(kubectl get pod "$OLD_POD" -n terminating-lab -o jsonpath='{.metadata.uid}')

Delete and wait until the original object is gone:

bash
kubectl delete pod "$OLD_POD" -n terminating-lab

Wait for the replacement:

bash
kubectl wait pod -n terminating-lab -l app=web --for=condition=Ready --timeout=120s
bash
NEW_POD=$(kubectl get pods -n terminating-lab -l app=web -o jsonpath='{.items[0].metadata.name}')
bash
NEW_UID=$(kubectl get pod "$NEW_POD" -n terminating-lab -o jsonpath='{.metadata.uid}')
bash
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


References


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.


Frequently Asked Questions

1. Why is my Kubernetes Pod stuck in Terminating?

The API server set a deletion timestamp but something blocks removal—kubelet shutdown still running, a finalizer waiting on cleanup, volume detach on a slow node, or an unreachable node that cannot confirm stop. Read deletionTimestamp, grace period, finalizers, and events before force deleting.

2. What does kubectl delete pod --force do?

With --grace-period=0, --force requests immediate API deletion without waiting for kubelet confirmation. The object can still remain when finalizers block deletion, and the container process may continue running on an unreachable node.

3. How long should Terminating last?

Brief Terminating during graceful shutdown is normal—commonly up to the configured grace period. The preStop hook runs inside that same time budget; Kubernetes may request a one-time two-second extension when the hook is still running as the period expires. Stuck beyond several minutes usually means finalizers, volume detach, or node communication problems.

4. Should I remove Pod finalizers to fix Terminating?

Only when you confirm the responsible controller cannot finish cleanup and the Pod is orphaned or broken. Finalizers block deletion until their owner completes work. Removing them skips that cleanup and can leave external resources inconsistent.

5. Will deleting a Deployment Pod recreate it?

Yes. The Deployment controller notices the missing replica and creates a replacement Pod with a new UID. That is expected behaviour—not a stuck Terminating case unless the old object never disappears.

6. Does --grace-period=10 force kill the container immediately?

No. It sets the maximum graceful-shutdown budget to ten seconds. The kubelet still runs preStop and sends the stop signal before hard kill, but it does not make Kubernetes wait ten seconds when the process exits earlier. Immediate API removal requires --grace-period=0 with --force.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)