A PVC stuck in Pending means nothing on the cluster can hand it a volume. Here's how we read the events and get it bound fast.
A PVC sits in Pending for exactly one reason: nothing on the cluster has handed it a volume yet. Either no existing PersistentVolume matches the claim, or the dynamic provisioner that was supposed to create one hasn't. The pod that needs the storage will sit in Pending right alongside it, and you'll get paged because a deploy never came up.
The good news: the PVC tells you why, if you ask it. The bad news is the message is buried in the events, and half the teams I work with never run the one command that surfaces it.
When you create a PVC, the control plane tries to satisfy it. With static provisioning it looks for a pre-created PV whose capacity, access mode, and storage class match. With dynamic provisioning it calls the CSI driver named by the StorageClass and asks it to cut a fresh volume. Pending means neither path finished. It is not a transient "wait a bit" state most of the time. It's a mismatch or a missing piece, and it will stay Pending forever until you change something.
Skip kubectl get. It only tells you it's Pending, which you already know. Go straight to the events:
kubectl describe pvc data-postgres-0 -n prod
The bottom of that output is where the answer lives:
Name: data-postgres-0
Namespace: prod
StorageClass: fast-ssd
Status: Pending
Volume:
Access Modes:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning ProvisioningFailed 12s (x8 over 2m) persistentvolume-controller storageclass.storage.k8s.io "fast-ssd" not found
There it is. The claim asked for a StorageClass called fast-ssd and no such class exists on this cluster. Every root cause below shows up as a distinct event line, so read them before you touch anything.
Then list what you actually have:
kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE DEFAULT
gp3 (default) ebs.csi.aws.com Delete WaitForFirstConsumer true
standard ebs.csi.aws.com Delete Immediate false
No matching StorageClass, or no default. This is the "fast-ssd" not found case above. Either the class name in the PVC is wrong, or you left storageClassName blank expecting a default and no class is marked default. Check the DEFAULT column. If nothing says (default), a class-less PVC has nowhere to go. Fix it by pointing the PVC at a real class, or annotate one class as default:
kubectl annotate storageclass gp3 \
storageclass.kubernetes.io/is-default-class=true
The CSI driver isn't installed or is failing. The class exists but provisioning still fails with something like failed to provision volume with StorageClass "gp3": rpc error ... driver name ebs.csi.aws.com not found. That means the controller can't reach the CSI driver. Check the driver pods:
kubectl get pods -n kube-system -l app=ebs-csi-controller
kubectl get csidrivers
If the controller is crash-looping, the usual culprit is IAM: the driver's service account can't create volumes in the cloud. Read the controller logs, fix the role, and the queued PVCs provision on their own.
Requested size or access mode isn't available. With static PVs, no persistent volumes available for this claim and no storage class is set means you're relying on hand-made PVs and none is big enough or free. The other classic is access mode. A block store like EBS or a plain GCP PD is ReadWriteOnce only. Ask for ReadWriteMany on it and no PV will ever match:
Warning ProvisioningFailed ... invalid AccessModes [ReadWriteMany]: only ReadWriteOnce supported
RWX needs a file backend: EFS, Filestore, or an NFS-backed class. Don't try to bend a block volume into shared access. Pick the right backend.
WaitForFirstConsumer, no consumer. Look back at that StorageClass table. gp3 uses WaitForFirstConsumer. That binding mode is deliberate: the volume isn't provisioned until a pod that mounts the PVC gets scheduled, so the volume lands in the same zone as the pod. If you create a bare PVC and stare at it, it will stay Pending with:
Normal WaitForFirstConsumer ... waiting for first consumer to be created before binding
This is not a bug. It's Pending-by-design. Create the pod that uses it. If the pod itself won't schedule (no capacity, taints, unsatisfiable node selector), the PVC waits on the pod, so go debug the pod's scheduling first.
Zone mismatch. With multi-zone clusters and Immediate binding, a volume can get provisioned in zone us-east-1a while the only node that can run the pod is in 1b. EBS volumes don't cross zones, so the pod stays Pending with volume node affinity conflict. This is the exact problem WaitForFirstConsumer was built to prevent, which is why I default every block-storage class to it.
Quota. A ResourceQuota on the namespace capping requests.storage or persistentvolumeclaims will block new claims with a quota-exceeded event. Run kubectl describe resourcequota -n prod and either raise the cap or clean up abandoned PVCs.
Here's a PVC and a class that bind cleanly on AWS:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
type: gp3
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-postgres-0
namespace: prod
spec:
storageClassName: gp3
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 50Gi
The data-postgres-0 failure at the top was just storageClassName: fast-ssd where the cluster only had gp3. Changing that one line and re-applying cleared it. The PVC bound the moment the StatefulSet pod scheduled.
If you want a broader map of cluster failure modes, our Kubernetes troubleshooting guide covers the neighboring ones.
Run describe first, every time, and read the events before you change a single YAML line. Nine times out of ten the message names the exact problem, and the tenth is a CSI driver or IAM issue that the driver's own logs will hand you. Default your block-storage classes to WaitForFirstConsumer so zone mismatches stop happening, mark exactly one StorageClass as default, and stop reaching for ReadWriteMany on volumes that were never built to share.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A team was burning 40,000 CI minutes a month and could not say why. Here is how GitHub Actions billing actually works and where the money leaks.
Static keys leak. The question isn't if but how fast you notice and how clean your response runbook is when the pager goes off.
Explore more articles in this category
Every CI/CD platform claims to be fast and easy. The real differences are in pricing, self-hosting, and where each one falls apart at scale. This is the map.
Woodpecker forked Drone when the license changed. Here's how the two compare for small teams and homelabs that just want simple container-native CI.
Buildkite runs the control plane and lets you own the compute; Actions keeps everything close to your repo. Here's how they actually differ once you scale.
Evergreen posts worth revisiting.