A container is ephemeral by design, so a PersistentVolumeClaim is the handle that pins real storage to it even when the Pod is recreated. The split is deliberate: a PersistentVolume declares capacity that exists, a PersistentVolumeClaim requests capacity that is needed, and a controller matches one to the other. With a StorageClass the claim can create its own volume through a provisioner, so nobody has to pre-build the storage.

The part that bites is that claim and volume have separate lifetimes governed by separate rules. A finalizer decides when the claim is allowed to disappear, and a reclaim policy decides what happens to the volume afterwards. Get the deletion order wrong and you get a claim stuck in Terminating; get the policy wrong and you get either data you meant to keep or a bill for storage nobody is using.

The claim, the volume, and the binding

Binding is a conjunction, not a best match. A volume is a candidate only if its capacity is at least the request, its access modes include the requested mode, its storageClassName matches, and no other claim owns it. A claim that satisfies nothing stays Pending with an event naming the requirement that failed, which is more useful than a generic failure; the usual causes are an access mode the class cannot deliver and a class name that does not exist.

accessModes has no default; you must choose one, and ReadWriteOnce is what most single-node block provisioners actually deliver. It is also single-node, so a StatefulSet with replicas on different nodes cannot share one claim. When a class supports dynamic provisioning, volumeBindingMode sets the timing: Immediate provisions the volume the moment the claim appears, which can place a topology-constrained volume in a zone with no node to use it, while WaitForFirstConsumer keeps the claim Pending until a pod is scheduled and then provisions in that pod’s zone.

The deletion order and the finalizer

The order is pod, then claim, then volume, and the protection is implemented with finalizers. Deleting a claim while a pod still mounts it does not delete anything: the claim gets a deletion timestamp, flips to Terminating, and stops there because kubernetes.io/pvc-protection is still in its finalizer list. The controller removes that finalizer only once no pod references the claim, so the “Terminating forever” PVC is not a stuck controller but a claim something still uses.

The same protection exists in the other direction. A PersistentVolume carries kubernetes.io/pv-protection, so deleting a volume that is bound to a claim is postponed until it is no longer bound. That ordering makes an accidental kubectl delete pv recoverable, and explains why a volume can seem to ignore a delete for as long as its claim exists.

Reclaim policy: Delete or Retain

When the claim is finally gone the volume is released, and the reclaim policy takes over. Delete removes the PV object and the external storage asset with it. Retain keeps the PV object, marks it Released, and deliberately does not make it available for a new claim, because the previous claimant’s data is still on it — reclamation becomes a manual sequence: delete the PV, clean the storage asset, delete the asset.

Dynamically provisioned volumes inherit the policy from their StorageClass, whose default is Delete. That default is right for scratch and wrong for a database, and it is invisible: nothing in the PVC or pod spec mentions the policy, so the decision lives in a cluster-scoped object nobody reviewing the workload ever sees.

When it breaks

The classic failure is the claim that will not die. Someone deletes a PVC, sees Terminating, and waits — while a pod somewhere still mounts it, usually a StatefulSet that was never scaled down or a pod stuck on an unreachable node. Nothing errors, nothing times out, and the PVC blocks the storage behind it. Find the pod rather than deleting the finalizer by hand; removing it while a pod still writes is how you detach a volume under a running process.

The second failure is the Retain orphan. The claim is gone, the pods are gone, the namespace may be gone, and a Released PV plus its storage asset sit there indefinitely. Nothing is in use and nothing is in error, so no alert fires and the volume does not appear in any workload’s list. The inverse mistake is just as quiet: a class whose default Delete policy was never overridden, discovered only when someone tries to recover data that the reclaim already removed.

Check it quickly

Look at both sides of the binding, and at what holds the claim:

kubectl get pvc -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,STATUS:.status.phase,VOLUME:.spec.volumeName,CLASS:.spec.storageClassName
kubectl get pv -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,POLICY:.spec.persistentVolumeReclaimPolicy,CLAIM:.spec.claimRef.name
kubectl get pvc data -o jsonpath='{.metadata.finalizers}{"\n"}'
kubectl get pods -A -o json | jq -r '.items[] | select(.spec.volumes[]?.persistentVolumeClaim.claimName=="data") | "\(.metadata.namespace)/\(.metadata.name)"'

A claim in Terminating whose finalizers still contain kubernetes.io/pvc-protection is being held by a pod, and the fourth command names it. On the volume side, a PV stuck in Released with a Retain policy is an orphan: nothing references it, nothing will reuse it, and only manual reclamation clears it.

Sources

  • Persistent Volumes, binding, access modes, reclaim policies, Released state and the pvc/pv protection finalizers, checked 2026-09-16.
  • Storage Classes, dynamic provisioning, reclaimPolicy, and volumeBindingMode, checked 2026-09-16.