OnCallReady

Kubernetes · 4 min read

PVC stuck in Terminating: the pvc-protection finalizer and what still uses it

A deleted PVC that stays Terminating is protected, not broken. Find the pod that still mounts it, and why removing the finalizer is the wrong fix.

terminal
$ 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>                 60m

Someone 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 delete waits 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

terminal
$ 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:

terminal
$ 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-debug

Used By lists the pods that hold it. To see every pod and its claims (handy across a whole namespace):

terminal
$ kubectl get pods -n cleanup-lab -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.volumes[*].persistentVolumeClaim.claimName}{"\n"}{end}'
forgotten-debug	old-data

3. Find out what the pod is, and who owns it

terminal
$ kubectl get pod forgotten-debug -n cleanup-lab
NAME              READY   STATUS    RESTARTS   AGE
forgotten-debug   1/1     Running   0          60m

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

terminal
$ 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>                          59m

Delete means the disk goes with the claim. If anyone might still want that data, switch it now, while the claim is still waiting:

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

terminal
$ 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 found

Deleting 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, or kubectl run --rm -it, so they don't outlive the session that created them.
  • Before deleting a claim, run kubectl describe pvc and read Used By.
  • Use Retain on 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.

OnCallReady is free, with no ads and no tracking. RSS · All posts