The Problem
You run kubectl delete namespace my-namespace and the namespace just... hangs. Status shows Terminating. You wait. Ten minutes later, it's still there. When you try to interact with it:
Error from server: namespaces "my-namespace" is being deleted
You can't recreate it, can't deploy into it, can't touch it at all. Ask any Kubernetes admin and they've hit this β usually at the worst possible moment, like mid-deployment or right before a demo.
Why This Happens
Kubernetes uses finalizers to control deletion order. When you delete a namespace, Kubernetes stamps a deletion timestamp and starts cleaning up everything inside. The namespace won't actually disappear until every finalizer is satisfied.
Here's what typically gets in the way:
- A custom resource (CRD) still exists, but its controller is gone β so the finalizer never runs
- An admission webhook is unreachable and silently blocking the deletion
- A persistent volume is stuck in a cleanup loop
- A third-party operator (cert-manager, Istio, Prometheus Operator) installed finalizers that were never removed
Start by inspecting the namespace's finalizers directly:
kubectl get namespace my-namespace -o yaml
Look for the finalizers field under spec. A stuck namespace usually looks like this:
spec:
finalizers:
- kubernetes
- some-operator.io/cleanup
Also scan for stuck resources inside the namespace:
kubectl api-resources --verbs=list --namespaced -o name \
| xargs -I{} kubectl get {} -n my-namespace --ignore-not-found 2>/dev/null
Quick Fix: Remove the Finalizer via kubectl patch
Strip out the finalizers and Kubernetes will complete the deletion immediately. Try this first:
kubectl patch namespace my-namespace \
-p '{"spec":{"finalizers":[]}}' \
--type=merge
If the namespace is already deep in the deleting state, that command may fail. Use the API proxy method instead β it bypasses the issue entirely.
Method 2: API Proxy (most reliable)
Start the proxy in one terminal:
kubectl proxy &
In a second terminal, export the namespace spec to a file:
kubectl get namespace my-namespace -o json > /tmp/ns.json
Open /tmp/ns.json, find the spec block, and replace its contents with:
"spec": {
"finalizers": []
}
Push the change through the finalize endpoint:
curl -s -o /dev/null -w "%{http_code}" \
-X PUT \
-H 'Content-Type: application/json' \
--data-binary @/tmp/ns.json \
http://127.0.0.1:8001/api/v1/namespaces/my-namespace/finalize
HTTP 200 means it worked. The namespace should disappear within 5β10 seconds.
Kill the proxy when done:
kill %1
Step-by-Step: Full Cleanup Workflow
Step 1 β Check what's actually stuck
# Namespace status and conditions
kubectl describe namespace my-namespace
# Recent events sorted by time
kubectl get events -n my-namespace --sort-by='.lastTimestamp'
# Everything running inside the namespace
kubectl get all -n my-namespace
Step 2 β Try to clean up resources manually first
Force-deleting the namespace is a last resort. Clean up stuck resources individually first β it's safer and avoids leaving orphaned objects behind.
# Force delete a stuck pod (skips the 30s grace period)
kubectl delete pod stuck-pod-name -n my-namespace --grace-period=0 --force
# Remove a finalizer from a specific resource
kubectl patch pod stuck-pod-name -n my-namespace \
-p '{"metadata":{"finalizers":null}}' \
--type=merge
Got custom resources? Find them first:
kubectl get crds | grep my-namespace
Patch their finalizers out the same way:
kubectl patch myresource resource-name -n my-namespace \
-p '{"metadata":{"finalizers":[]}}' \
--type=merge
Step 3 β Force remove the namespace finalizer
Once stuck resources are cleared β or if you can't clear them β use the API proxy method above to wipe the namespace-level finalizer.
Step 4 β Verify the fix
# Should return NotFound if fully deleted (that's the good outcome)
kubectl get namespace my-namespace
# Expected output:
# Error from server (NotFound): namespaces "my-namespace" not found
# Double-check it's gone from the list
kubectl get namespaces | grep my-namespace
Need to recreate it right away?
kubectl create namespace my-namespace
Preventing This in the Future
Nine times out of ten, a stuck namespace traces back to an operator that installed finalizers and was removed without cleaning them up. A few habits that prevent this:
- Uninstall operators before deleting namespaces. For Helm-managed operators,
helm uninstallhandles cleanup automatically. For manual installs, check the operator docs for the teardown procedure. - Audit admission webhooks. Webhooks matching your namespace can silently block deletion. Run
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurationsand look for any that target the namespace you're deleting. - Add a pre-delete script to CI/CD pipelines. Before any namespace deletion in automation, run a script that patches out known finalizers. Catches the problem before it becomes an incident at 2am.
- Delete CRD instances before removing the operator. Uninstalling cert-manager or Istio while their CRD objects still exist in a namespace leaves those objects orphaned with finalizers that can never be satisfied.
When Nothing Works
Still stuck? Check whether a validating webhook is intercepting the finalize call:
kubectl get validatingwebhookconfigurations -o yaml | grep -A5 my-namespace
Delete the offending webhook temporarily, then retry the curl command from Method 2.
On managed clusters β EKS, GKE, AKS β some system webhooks get automatically reinstalled by the control plane. You can't safely remove those without breaking other things. If the webhook belongs to a cluster-managed component, open a support ticket with your cloud provider. It's the only clean path forward.

