$ kubectl get pvc -n cleanup-lab
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
old-data Terminating pvc-f67aaaff-a577-4dda-ad19-97cc71b14c56 1Gi RWO lab-disk <unset> 60mSomeone deleted the claim old-data an hour ago. It's still there, Terminating, and the question in the channel is the usual one: "Can I just remove the finalizer?"
Don't, at least not yet. Nothing in the API is broken. The claim is doing what it was built to do. It's a cousin of the Linux classic where a deleted log doesn't free disk space: storage that's still in use doesn't go away just because you deleted its name.
What is actually happening
In Kubernetes, deleting an object is a request, not an action. When an object has finalizers, a list of keys in metadata.finalizers, kubectl delete only sets metadata.deletionTimestamp. The object then shows as Terminating, and it's only removed once every finalizer has been taken off its list. Each finalizer belongs to a controller, and that controller removes its key when it decides the object can safely go.
Every PersistentVolumeClaim gets the finalizer kubernetes.io/pvc-protection. The controller behind it removes the key only when no pod is using the claim. That's storage-in-use protection: Kubernetes won't delete a claim out from under a running pod. Bound PersistentVolumes have the same thing, kubernetes.io/pv-protection, while a claim still holds them.
Two side effects confuse people:
kubectl deletewaits for the finalizers by default (--wait=true), so the command seems to hang. Ctrl+C stops kubectl, not the deletion. The claim is already on its way out, and there's no undo.- A new pod that references the claim won't start while the claim is being deleted. It stays Pending (the FailedScheduling event names the claim). So if a Deployment or StatefulSet owns the pod, deleting the pod alone isn't enough.
Diagnosis
1. Confirm it's a finalizer wait
$ kubectl get pvc old-data -n cleanup-lab -o jsonpath='{.metadata.deletionTimestamp}{" "}{.metadata.finalizers}{"\n"}'
2026-09-22T19:10:03Z ["kubernetes.io/pvc-protection"]A deletion timestamp plus a remaining finalizer means "deletion requested, waiting for permission".
2. Ask who's using it
kubectl describe pvc answers directly:
$ kubectl describe pvc old-data -n cleanup-lab
Name: old-data
Namespace: cleanup-lab
StorageClass: lab-disk
Status: Terminating (lasts 50m)
Volume: pvc-f67aaaff-a577-4dda-ad19-97cc71b14c56
...
Finalizers: [kubernetes.io/pvc-protection]
Capacity: 1Gi
Access Modes: RWO
VolumeMode: Filesystem
Used By: forgotten-debugUsed By lists the pods that hold it. To see every pod and its claims (handy across a whole namespace):
$ kubectl get pods -n cleanup-lab -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.volumes[*].persistentVolumeClaim.claimName}{"\n"}{end}'
forgotten-debug old-data3. Find out what the pod is, and who owns it
$ kubectl get pod forgotten-debug -n cleanup-lab
NAME READY STATUS RESTARTS AGE
forgotten-debug 1/1 Running 0 60mA debug pod someone forgot. Check metadata.ownerReferences before you act. A bare pod like this one has none. If a Deployment, StatefulSet or Job owns the pod, deleting the pod just gets you a new one. Scale the owner down, or remove the volume from its template.
4. Decide what happens to the data
The PV's reclaim policy decides what happens to the disk once the claim is gone:
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
pvc-f67aaaff-a577-4dda-ad19-97cc71b14c56 1Gi RWO Delete Bound cleanup-lab/old-data lab-disk <unset> 59mDelete means the disk goes with the claim. If anyone might still want that data, switch it now, while the claim is still waiting:
kubectl patch pv pvc-f67aaaff-a577-4dda-ad19-97cc71b14c56 -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'With Retain, the PV and the disk survive as Released, and an admin can recover them.
The fix
Answer the finalizer's question instead of deleting it. Remove the user of the claim:
$ kubectl delete pod forgotten-debug -n cleanup-lab
pod "forgotten-debug" deleted from cleanup-lab namespace
$ kubectl get pvc -n cleanup-lab
No resources found in cleanup-lab namespace.
$ kubectl get pv
No resources foundDeleting the pod has its own short wait, the grace period (what happens between kubectl delete pod and the container disappearing). Once the pod is gone, the claim finishes deleting by itself within seconds. With the Delete policy, the volume goes with it.
Why not just remove the finalizer?
kubectl patch pvc old-data --type=merge -p '{"metadata":{"finalizers":null}}' makes the claim disappear immediately while the pod still has the volume mounted and keeps writing to it. With reclaim Delete, the provisioner is then told to delete a disk that's in use. Depending on the storage driver, you either lose the data under a running app, or the delete fails and leaves an orphaned volume that nothing in the cluster tracks any more.
A finalizer is a question: "is it safe yet?". Removing it by hand is only right when the controller that would answer is gone for good, for example an uninstalled operator whose custom resources are stuck. pvc-protection is part of the control plane, and it's answering. The answer is "no, forgotten-debug still uses it".
How to prevent it
- Give debug pods a lifetime:
activeDeadlineSeconds, orkubectl run --rm -it, so they don't outlive the session that created them. - Before deleting a claim, run
kubectl describe pvcand readUsed By. - Use
Retainon StorageClasses for data you can't recreate. Deleting a claim then can't delete the disk. - When something is stuck in Terminating, look at its finalizers first, then at the controller that owns each one.
Practise it
The storage chapter's incident "the PVC will not die" leaves a claim Terminating behind a pod someone forgot. You resolve it without touching the finalizer, and the same chapter's lessons cover reclaim policies and protection.