Pod Has Unbound PersistentVolumeClaims: The Real Checklist
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'sstorageClassNameexist, 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.
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. Checkkubectl get pvand 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 conflictat 3am. - "Just remove the finalizer." The finalizer is a safety interlock, not a bug. Find the referencing pod instead.
When it is still broken
- Read cluster-wide events in time order.
kubectl get events -A --sort-by=.metadata.creationTimestamp | tail -40often shows the provisioner's real complaint that never made it onto the PVC. - 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.
- 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.
- 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.