</>CodeWithKarani

Pod Has Unbound PersistentVolumeClaims: The Real Checklist

Karani GeoffreyKarani Geoffrey7 min read

A pod sits in Pending. You describe it, and the scheduler says the pod has unbound PersistentVolumeClaims. So you describe the PVC, and it says Pending too, with an event that reads like it is waiting for something you have already created. Then someone suggests deleting the PVC and letting it recreate, and if the StorageClass had reclaimPolicy: Delete, that is how a database loses its data at 2am.

This error is not one problem. It is four unrelated problems that share one message, and the message is generated by the scheduler, which is the component furthest from the actual cause. The fix is to stop reading the pod and start reading the storage layer.

run these three commands in order, and stop at the first one that shows something wrong.

  • kubectl describe pvc <name> - read the Events block at the bottom, not the status field.
  • kubectl get storageclass - does the PVC's storageClassName exist, and is there a default marked?
  • kubectl get pods -n kube-system | grep csi - is the provisioner actually running?

If the PVC event says "waiting for first consumer", the storage layer is fine and your pod cannot be scheduled for an unrelated reason. Fix the scheduling problem and the PVC binds by itself.

The exact errors

From kubectl describe pod:

Warning  FailedScheduling  12s (x4 over 2m)  default-scheduler
  0/3 nodes are available: pod has unbound immediate PersistentVolumeClaims.
  preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.

Or the topology variant, which people misread as the same thing:

Warning  FailedScheduling  30s  default-scheduler
  0/6 nodes are available: 6 node(s) had volume node affinity conflict.

And from kubectl describe pvc, the three events that matter:

Normal   WaitForFirstConsumer  waiting for first consumer to be created before binding

Normal   ExternalProvisioning  waiting for a volume to be created, either by external
                               provisioner "ebs.csi.aws.com" or manually created by
                               system administrator

Warning  ProvisioningFailed    storageclass.storage.k8s.io "fast-ssd" not found

Those three mean completely different things and require completely different fixes.

Why one message covers four causes

Volume binding sits between two controllers that do not talk to each other directly. The scheduler will not place a pod whose volumes are not resolvable. The provisioner will not create a volume until it knows where the pod is going, if the StorageClass says so. The result is that any failure anywhere in that loop surfaces to you as "unbound", including the case where nothing is wrong with storage at all.

kubectl describe pvc: read Events "storageclass not found" Typo, or no default class "waiting for a volume" CSI driver down or quota hit "waiting for first consumer" Storage is fine. Fix the pod. No events at all Static PV mismatch kubectl get storageclass Match the name exactly. Check (default) marker. kubectl logs the CSI controller pod in kube-system. Why can't the pod schedule? Taints, quota, nodeSelector, resources. Compare size, accessModes and class on PV vs PVC. Never in this tree: delete and recreate the PVC. With reclaimPolicy Delete, that destroys the underlying disk and the data on it.
The event text on the PVC is the branch point. The pod's error message tells you nothing about which branch you are on.

Cause 1: the StorageClass does not exist

Either the name is wrong, or the PVC omitted storageClassName and the cluster has no default class. A bare kubeadm cluster has no default class at all, which is why every "works on EKS, breaks on my cluster" chart hits this.

kubectl get storageclass
NAME            PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      AGE
gp3 (default)   ebs.csi.aws.com         Delete          WaitForFirstConsumer   112d

Note the (default) marker. If no class carries it and your PVC does not name one, the PVC will never bind. Also watch for storageClassName: "", an empty string, which explicitly disables dynamic provisioning and means "only bind to a pre-created PV". That is not the same as leaving the field out.

Cause 2: the provisioner is not running

The StorageClass exists but nothing is listening for it. The PVC will sit on ExternalProvisioning forever, blaming a provisioner that is crash-looping or missing its cloud credentials.

kubectl get csidrivers
kubectl -n kube-system get pods | grep -i csi
kubectl -n kube-system logs deploy/ebs-csi-controller -c csi-provisioner --tail=50

The provisioner log is where the real error lives: an IAM policy denial, a volume quota, an unsupported volume type for the instance family. The PVC event never repeats it.

Cause 3: WaitForFirstConsumer plus a pod that cannot schedule

This is the chicken-and-egg case, and it is the one that wastes the most time. With volumeBindingMode: WaitForFirstConsumer, the volume is deliberately not created until the scheduler picks a node, so the provisioner can put the disk in the right zone. If the pod cannot be scheduled for a reason that has nothing to do with storage, the volume is never provisioned, and the only error you get back is about volumes.

The tell is a PVC in Pending whose only event is waiting for first consumer to be created before binding. That event is normal. Go and read why the pod cannot schedule:

kubectl describe pod <pod> | sed -n '/Events:/,$p'
kubectl describe node <node> | grep -A5 Taints
kubectl get resourcequota -n <namespace>

Insufficient CPU, an unsatisfiable nodeSelector, a taint with no matching toleration, a namespace resource quota, or a topology spread constraint. Fix that, and the PVC binds within seconds without you touching storage at all.

Cause 4: zone and topology mismatch

Zonal block storage cannot cross availability zones. If a PV was created in eu-west-1a and the pod is now being scheduled somewhere else, you get volume node affinity conflict rather than "unbound", but the root cause belongs in the same family. It happens most often when a node group scales down and the replacement nodes come up in a different zone, or when a StatefulSet pod is rescheduled after its original node is gone.

kubectl get pv <pv-name> -o jsonpath='{.spec.nodeAffinity}' | python3 -m json.tool
kubectl get nodes -L topology.kubernetes.io/zone

The fix is architectural: either keep a node pool in every zone your volumes live in, or use a storage class whose volumes are not zonal (EFS, a networked filesystem, or a replicated storage layer). A WaitForFirstConsumer class prevents new instances of this problem but cannot rescue a volume that already exists in the wrong place.

Verification

kubectl get pvc -n <ns>
NAME            STATUS   VOLUME                CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-pg-0       Bound    pvc-6b1e...           20Gi       RWO            gp3            40s

Bound with a VOLUME name filled in is the only acceptable state. Then confirm the pod actually mounted it, rather than merely being scheduled:

kubectl exec -n <ns> <pod> -- df -h /var/lib/postgresql/data

Expect a filesystem of roughly the requested size mounted at the path, not the container's overlay root. If you see the overlay filesystem, the volume bound but the mount path in the manifest is wrong, and your data is being written into the container layer where it will vanish on restart.

The PVC that will not delete

The other half of this problem: you delete a StatefulSet and its PVC hangs in Terminating forever.

kubectl get pvc data-pg-0 -o jsonpath='{.metadata.finalizers}'
["kubernetes.io/pvc-protection"]

That finalizer exists to stop you deleting storage out from under a running pod. It is doing its job. Some pod, somewhere, still references the claim, and it may not be the one you were thinking about: a completed Job, a paused Deployment, a leftover debug pod.

kubectl get pods -n <ns> -o json | \
  jq -r '.items[] | select(.spec.volumes[]?.persistentVolumeClaim.claimName=="data-pg-0") | .metadata.name'

Delete or scale down whatever that returns, and the PVC finishes deleting on its own. Removing the finalizer by hand is the move of last resort:

# Only after confirming nothing references the claim.
kubectl patch pvc data-pg-0 -p '{"metadata":{"finalizers":null}}' --type=merge

Doing this while a pod still has the volume mounted leaves the PV and the cloud disk orphaned, and on some CSI drivers leaves a node with a stale attachment that blocks future pods from mounting anything.

What people get wrong

  • "Delete the PVC and let it recreate." With reclaimPolicy: Delete, which is the default on nearly every cloud StorageClass, deleting the PVC deletes the disk. For a database that is total data loss with no undo. Check kubectl get pv and the RECLAIM POLICY column before you type anything destructive.
  • "Set accessModes to ReadWriteMany, that fixes binding." Only if your storage backend actually supports it. Block storage such as EBS or a GCE persistent disk does not. Asking for RWX on a class that cannot provide it makes binding impossible rather than easier.
  • "Switch the StorageClass to Immediate binding." This makes the symptom disappear and creates a worse one: the volume gets created in whatever zone the provisioner picks, and later the pod cannot be scheduled there. You have traded a clear error at creation time for volume node affinity conflict at 3am.
  • "Just remove the finalizer." The finalizer is a safety interlock, not a bug. Find the referencing pod instead.

When it is still broken

  1. Read cluster-wide events in time order. kubectl get events -A --sort-by=.metadata.creationTimestamp | tail -40 often shows the provisioner's real complaint that never made it onto the PVC.
  2. Check quota at the cloud provider. Volume count and total provisioned GiB limits are per region and are easy to hit on a busy cluster. The provisioner log names the quota.
  3. Confirm the node has the CSI node plugin. A node missing the DaemonSet pod will accept the scheduling decision and then fail to mount, which looks like a different problem entirely.
  4. For a StatefulSet, remember its PVCs outlive it. Deleting the StatefulSet leaves the claims behind by design so your data survives. Recreating the StatefulSet reattaches them, which is usually what you want and occasionally very surprising if the old claims have the wrong size.

The habit that prevents most of this: when a pod is Pending, never stop at the pod. The pod is the last component in the chain and it is reporting on the failure of the first.

Frequently asked questions

What does 'pod has unbound immediate PersistentVolumeClaims' actually mean?
It means the scheduler refuses to place the pod because at least one of its PersistentVolumeClaims is not yet bound to a PersistentVolume. The message comes from the scheduler, which is the component furthest from the real cause, so it does not tell you whether the StorageClass is missing, the CSI provisioner is down, or the pod itself cannot be scheduled. Run kubectl describe pvc and read the Events block to find out which.
My PVC says 'waiting for first consumer to be created before binding'. Is that an error?
No, that event is normal for a StorageClass with volumeBindingMode WaitForFirstConsumer. It means Kubernetes is deliberately delaying volume creation until the scheduler picks a node, so the disk is created in the right zone. If the PVC stays that way, the real problem is that your pod cannot be scheduled for an unrelated reason such as insufficient CPU, a taint, or a namespace quota.
Is it safe to delete and recreate a stuck PVC?
Usually not. Most cloud StorageClasses default to reclaimPolicy Delete, so deleting the PVC deletes the PersistentVolume and the underlying disk along with all its data. Check the RECLAIM POLICY column in kubectl get pv first, and never do this on a database claim as a debugging step.
How do I fix a PVC stuck in Terminating?
The kubernetes.io/pvc-protection finalizer is holding it because some pod still references the claim, often a completed Job or a forgotten debug pod rather than the workload you were thinking of. List pods whose spec.volumes reference the claim name, delete or scale those down, and the PVC finishes deleting on its own. Patching the finalizer away should be a last resort because it can leave orphaned disks and stale node attachments.
#Kubernetes#PersistentVolume#CSI#StatefulSet#Storage
Keep reading

Related articles