Full lesson
Explore the full explanation, examples, and visuals at your own pace.
Illustrative terminal output
You deleted the namespace, kubectl said deleted, and it's still there as Terminating twelve days on. The API server isn't frozen. It accepted your request. Something inside that namespace is still waiting to finish, and until it does, Kubernetes won't let the namespace go.
$ kubectl delete namespace payments
namespace "payments" deleted
# ...command hangs
$ kubectl get namespace payments
NAME STATUS AGE
payments Terminating 12dStep 1: how namespace deletion works
A delete request doesn't remove the namespace. It sets a deletionTimestamp, and the namespace becomes Terminating. Then the namespace controller deletes every namespaced object inside. Only when the namespace is empty does the kubernetes finalizer come off, and the namespace disappears. Deletion is gated on the contents being gone.
Step 2: a finalizer on one resource
Take one object in payments, a Bucket named invoices-archive, carrying the finalizer storage.example.com/cleanup. Deleting it only marks it. The operator sees that, deletes the real cloud bucket, and then drops the finalizer. Only after that does the Bucket object actually vanish from Kubernetes.
What happens when the operator is down
Follow the blocked run. The namespace controller asks the API server to delete the Bucket, and the Bucket gets a deletion timestamp. The operator tries to delete the real cloud bucket, but the Cloud API rejects it with an auth failure, because the credentials expired. The finalizer stays, so the namespace controller just keeps rechecking.
The chain of waiting
The namespace isn't broken. It's waiting on the Bucket, which is held by a finalizer, which waits on the operator, which is blocked by expired cloud credentials. Terminating is just the visible end of that chain, so you follow it to the broken link.
Suppose the bucket operator ran inside the payments namespace and was deleted first. What happens to the Bucket resource?
Suppose the bucket operator ran inside the payments namespace and was deleted first. What happens to the Bucket resource?
- It stays Terminating; nobody removes its finalizer
- Kubernetes deletes the cloud bucket automatically
- The namespace skips it after a timeout
Suppose the bucket operator ran inside the payments namespace and was deleted first. What happens to the Bucket resource?
It stays Terminating; nobody removes its finalizer. Kubernetes itself never runs a custom finalizer's cleanup; only the owning controller does. With the operator gone, nothing deletes the cloud bucket or removes the finalizer, and there is no timeout, so the Bucket and namespace wait indefinitely.
- It stays Terminating; nobody removes its finalizer
- Kubernetes deletes the cloud bucket automatically
- The namespace skips it after a timeout
Namespace conditions that keep it waiting
The namespace tells you why it's waiting. Content remaining means objects still exist. Finalizers remaining means something still holds a finalizer. Discovery failure means an API group is unreachable, so Kubernetes can't even list what's inside to delete it.
- NamespaceContentRemaining: objects still exist
- NamespaceFinalizersRemaining: finalizers not removed
- NamespaceDeletionDiscoveryFailure: API group unreachable
Investigate step 1: ask the namespace
Start by asking the namespace itself. This command only reads, so it's safe. Two conditions matter: one names the blocking resource type, buckets, and the other names the finalizer, storage.example.com/cleanup. Exact wording varies by version, and these messages are trimmed, but the clues are the same.
$ kubectl get namespace payments -o yaml
status:
phase: Terminating
conditions:
- type: NamespaceContentRemaining
status: "True"
message: 'buckets.storage.example.com has 1 resource instances'
- type: NamespaceFinalizersRemaining
status: "True"
message: 'storage.example.com/cleanup in 1 resource instances'Investigate step 2: object, then controller
Now look at the object itself. The Bucket invoices-archive has a deletionTimestamp and a finalizer, so it's waiting on cleanup. Then ask who owns that finalizer. Checking the pods and logs in storage-system should show the operator crash-looping, with an expired credentials error.
$ kubectl get buckets -n payments -o yaml
metadata:
name: invoices-archive
deletionTimestamp: "2024-05-02T10:14:03Z"
finalizers:
- storage.example.com/cleanup
$ kubectl get pods -n storage-system
$ kubectl logs -n storage-system deploy/bucket-operatorTwo ways to unstick it
You have two ways out. Fix the operator's credentials, and it deletes the cloud bucket, removes the finalizer, and the namespace disappears cleanly. Strip the finalizer, and the object vanishes instantly, but the real bucket stays behind, unowned and still billing. Only strip it after verifying cleanup yourself.
Follow the chain to the blocked controller
Deletion in Kubernetes is a promise checked by finalizers. So read the namespace conditions, find the finalizer's owner, and fix that controller. Strip a finalizer only after you've confirmed the external cleanup yourself, or you'll orphan what it was protecting.




