InfraRunBook
    Back to articles

    Kubernetes PersistentVolume Not Binding

    Kubernetes
    Published: Apr 12, 2026
    Updated: Apr 12, 2026

    Step-by-step troubleshooting guide for Kubernetes PersistentVolumeClaims stuck in Pending state, covering every root cause from missing StorageClasses to access mode and capacity mismatches.

    Kubernetes PersistentVolume Not Binding

    Symptoms

    You've deployed a workload that needs persistent storage, and the pod is stuck in Pending state. When you describe the pod, you see something like this:

    Events:
      Warning  FailedScheduling  30s   default-scheduler  0/3 nodes are available: pod has unbound immediate PersistentVolumeClaims.

    Or you look directly at the PVC and it's sitting in Pending instead of Bound:

    $ kubectl get pvc -n production
    NAME              STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
    app-data-claim    Pending                                      fast-ssd       8m

    The pod won't schedule, the workload is down, and you need to figure out why that PVC won't bind. This article walks through every common reason a PersistentVolumeClaim stays stuck in Pending and shows you exactly how to diagnose and fix each one.

    How PV Binding Works

    Before diving into causes, it helps to understand what Kubernetes is actually doing when it tries to bind a PVC. When you create a PVC, the control plane runs a binding loop that looks for a compatible PersistentVolume. For a match to succeed, the PV must satisfy all of the following: the StorageClass must match, the access modes must be compatible, the capacity must be sufficient, and the PV must not already be bound or reserved. If dynamic provisioning is in play, the StorageClass provisioner handles creating the PV automatically — but if the provisioner can't be found or fails, you end up in the same Pending state.

    Understanding this binding logic makes it much easier to narrow down what's wrong. Always start with

    kubectl describe pvc <name>
    — the Events section at the bottom usually tells you exactly which condition failed.

    Root Cause 1: StorageClass Not Found

    This one catches people off guard more often than you'd expect. The PVC references a StorageClass by name, but that StorageClass either doesn't exist in the cluster, was deleted, or was never created in the first place.

    Why does this happen? Usually it's a copy-paste from a manifest that worked in a different cluster — maybe staging had a StorageClass called

    fast-ssd
    and production has one called
    gp3-encrypted
    . Or the StorageClass was part of a Helm chart that wasn't installed. In my experience, this also comes up frequently after cluster migrations where the new cluster has a different set of provisioners and nobody updated the application manifests.

    To identify it, check what StorageClasses actually exist:

    $ kubectl get storageclass
    NAME                PROVISIONER                    RECLAIMPOLICY   VOLUMEBINDINGMODE      AGE
    standard (default)  kubernetes.io/no-provisioner   Delete          Immediate              45d
    gp3-encrypted       ebs.csi.aws.com                Delete          WaitForFirstConsumer   45d

    Now compare that against what your PVC is requesting:

    $ kubectl describe pvc app-data-claim -n production
    Name:          app-data-claim
    Namespace:     production
    StorageClass:  fast-ssd
    Status:        Pending
    ...
    Events:
      Warning  ProvisioningFailed  2m  persistentvolume-controller  storageclass.storage.k8s.io "fast-ssd" not found

    There it is. The StorageClass

    fast-ssd
    doesn't exist. The fix is straightforward: either update the PVC to reference a StorageClass that actually exists, or create the missing one. Here's an example for an EBS-backed class:

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: fast-ssd
    provisioner: ebs.csi.aws.com
    parameters:
      type: gp3
      encrypted: "true"
    volumeBindingMode: WaitForFirstConsumer
    reclaimPolicy: Delete

    If you're working with a static PV instead of dynamic provisioning, both the PVC and PV must specify the same StorageClass name, or both must omit it entirely. A PVC with no StorageClass will match a PV with no StorageClass — but a PVC with an explicit name and a PV with a different name simply won't bind, no matter how compatible they are in every other way.

    Root Cause 2: No Matching PV (Static Provisioning)

    When you're not using dynamic provisioning, you need a pre-created PV that matches the PVC. If no such PV exists — or all existing PVs have already been consumed — the PVC sits in Pending indefinitely.

    This comes up most often in on-premises clusters where storage isn't infinitely elastic. An engineer provisions a handful of PVs manually, the cluster grows, a new namespace gets added, and there simply aren't enough PVs to go around. Identify it by listing all PVs and their status:

    $ kubectl get pv
    NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                        STORAGECLASS   AGE
    pv-data-1  10Gi       RWO            Retain           Bound       production/db-claim          local-storage  30d
    pv-data-2  10Gi       RWO            Retain           Bound       production/cache-claim       local-storage  30d
    pv-data-3  5Gi        RWO            Retain           Available                                local-storage  30d

    Here

    pv-data-3
    is Available, but it's only 5Gi. If the PVC is requesting 10Gi, this PV won't match. The fix is to create a PV at the right size. For a local volume on node
    sw-infrarunbook-01
    :

    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: pv-data-4
    spec:
      capacity:
        storage: 10Gi
      accessModes:
        - ReadWriteOnce
      persistentVolumeReclaimPolicy: Retain
      storageClassName: local-storage
      local:
        path: /mnt/disks/ssd1
      nodeAffinity:
        required:
          nodeSelectorTerms:
            - matchExpressions:
                - key: kubernetes.io/hostname
                  operator: In
                  values:
                    - sw-infrarunbook-01

    After applying this, the binding loop will pick it up within seconds if all other criteria match the pending PVC.

    Root Cause 3: Access Mode Mismatch

    Access modes are one of the trickier aspects of PV binding because the names sound intuitive but the semantics have sharp edges. Kubernetes supports three access modes: ReadWriteOnce (RWO) allows mounting read-write by a single node, ReadOnlyMany (ROX) allows read-only mounting by multiple nodes, and ReadWriteMany (RWX) allows read-write mounting by multiple nodes.

    A PVC requesting

    ReadWriteMany
    will not bind to a PV that only supports
    ReadWriteOnce
    , even if the capacity and StorageClass match perfectly. The other common trap: not all storage backends support all access modes. EBS volumes only support RWO. NFS and CephFS support RWX. If you're requesting RWX on an EBS-backed StorageClass, you'll never get a binding — the provisioner either fails or produces a volume that won't satisfy the access mode requirement.

    Identify the mismatch by comparing the PVC spec against available PVs:

    $ kubectl describe pvc shared-data-claim -n production
    Name:          shared-data-claim
    Namespace:     production
    StorageClass:  local-storage
    Status:        Pending
    Access Modes:  RWX
    
    $ kubectl get pv pv-data-4 -o yaml | grep -A5 accessModes
      accessModes:
      - ReadWriteOnce

    The PVC wants RWX, the PV offers RWO. They won't match. The fix depends on what your application actually needs. If it genuinely requires shared access across multiple nodes, you need a storage backend that supports RWX — NFS, CephFS, Azure Files, and EFS are common choices. If the application doesn't actually need multi-node access, change the PVC to request

    ReadWriteOnce
    . Don't just change the access mode to make it bind without confirming the application's actual I/O pattern.

    Root Cause 4: Capacity Mismatch

    Kubernetes uses capacity as part of the binding decision. A PVC requesting 20Gi will not bind to a PV that offers only 10Gi. The reverse is acceptable — a PVC requesting 10Gi can bind to a 20Gi PV, though it wastes capacity. This asymmetry trips people up.

    I've seen clusters where someone created a 9Gi PV but the PVC asked for 10Gi, and the binding loop silently ignored the match for hours because nobody thought to check sizes. The describe output won't always scream at you — sometimes it just says no volumes are available.

    $ kubectl describe pvc app-data-claim -n production
    Name:          app-data-claim
    Namespace:     production
    StorageClass:  local-storage
    Status:        Pending
    Events:
      Normal  FailedBinding  3m  persistentvolume-controller  no persistent volumes available for this claim and no storage class is set
    
    $ kubectl get pv
    NAME       CAPACITY   ACCESS MODES   STATUS      STORAGECLASS
    pv-data-3  9Gi        RWO            Available   local-storage

    The PVC needs 10Gi, the only available PV is 9Gi. The fix is to create a PV at the correct size or, if you have flexibility in the PVC spec, adjust it downward to match what's available. For dynamic provisioning, a capacity mismatch usually means a quota issue or a misconfigured StorageClass — check your cloud provider's volume size minimums and maximums as well, since some providers enforce minimum sizes of 1Gi, 4Gi, or even 20Gi depending on volume type.

    If you're troubleshooting under pressure and the PVC hasn't been used yet, you can delete and recreate it at the correct size:

    $ kubectl delete pvc app-data-claim -n production
    $ kubectl apply -f pvc-corrected.yaml

    Never delete a PVC that's currently mounted by a running pod. Drain the workload first.

    Root Cause 5: PV Already Bound to Another PVC

    A PV in

    Bound
    status is exclusive — it belongs to exactly one PVC at a time. If the PV you're targeting is already claimed, your new PVC won't get it. This sounds obvious, but there's a subtler version of this problem worth knowing: a PV in
    Released
    state.

    When a PVC is deleted, the PV transitions to

    Released
    . The data is still there, the volume still exists, but Kubernetes won't automatically rebind it to a new PVC. This is by design — it protects you from accidentally binding a PV that contains data from a previous workload. But it also means a PV can look almost-available and still refuse to bind.

    $ kubectl get pv
    NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM                       STORAGECLASS
    pv-data-1  10Gi       RWO            Retain           Released   production/old-app-claim    local-storage

    That

    Released
    status means the PV still has its old
    claimRef
    populated. A new PVC can't claim it until you clear that reference. Here's how to fix it:

    $ kubectl patch pv pv-data-1 --type json -p '[{"op": "remove", "path": "/spec/claimRef"}]'
    persistentvolume/pv-data-1 patched
    
    $ kubectl get pv pv-data-1
    NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      STORAGECLASS
    pv-data-1  10Gi       RWO            Retain           Available   local-storage

    After removing the

    claimRef
    , the PV returns to
    Available
    and can match a new PVC. Be intentional here — only do this after you've confirmed the previous data is either no longer needed or has been backed up elsewhere.

    For the case where the PV is actively

    Bound
    to a different PVC in a different namespace:

    $ kubectl describe pv pv-data-1 | grep -E "Status|Claim"
    Status:          Bound
    Claim:           staging/other-app-claim

    That PV is owned by the

    staging
    namespace. Your only real options are to wait for that PVC to be deleted, create a new PV, or switch to dynamic provisioning so the cluster stops requiring you to manage this manually.

    Root Cause 6: VolumeBindingMode Set to WaitForFirstConsumer

    This one is increasingly common as more teams adopt topology-aware storage. When a StorageClass has

    volumeBindingMode: WaitForFirstConsumer
    , the PV is not provisioned or bound until a pod that uses the PVC is actually scheduled. The PVC will sit in Pending until then — and this is expected behavior, not a bug.

    $ kubectl get storageclass gp3-encrypted -o yaml | grep volumeBindingMode
      volumeBindingMode: WaitForFirstConsumer
    
    $ kubectl get pvc app-data-claim -n production
    NAME              STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS    AGE
    app-data-claim    Pending                                      gp3-encrypted   2m

    If the PVC is Pending but no pod referencing it has been created yet, this is exactly what's supposed to happen. Create the pod, and the PVC will bind as part of scheduling. If a pod already references the PVC and that pod is also stuck in Pending, the problem is likely at the node level — check pod events:

    $ kubectl describe pod app-pod -n production | tail -20
    Events:
      Warning  FailedScheduling  45s  default-scheduler  0/3 nodes are available: 1 node(s) had volume node affinity conflict, 2 node(s) didn't match Pod's node affinity/selector.

    That "volume node affinity conflict" message is your signal. The provisioner created the volume in one availability zone and your pod's nodeSelector or affinity rules point at a different zone. Aligning them resolves the binding.

    Root Cause 7: Label Selector Mismatch on PVC

    PVCs support a

    selector
    field that filters which PVs are eligible for binding. If you specify a selector that doesn't match any available PV's labels, the PVC stays Pending even when PVs exist with matching capacity, access modes, and StorageClass. This is easy to overlook because the selector isn't always visible in a quick
    kubectl get pvc
    .

    $ kubectl describe pvc app-data-claim -n production
    Selector:    environment=production,tier=database
    
    $ kubectl get pv --show-labels
    NAME       CAPACITY   STATUS      LABELS
    pv-data-3  10Gi       Available   environment=staging,tier=database

    The PV has

    environment=staging
    but the PVC requires
    environment=production
    . To fix it, relabel the PV if appropriate or remove the selector from the PVC if it isn't providing real filtering value:

    $ kubectl label pv pv-data-3 environment=production --overwrite
    persistentvolume/pv-data-3 labeled

    Prevention

    Most PV binding failures are preventable with a few consistent practices.

    Default to dynamic provisioning wherever your infrastructure supports it. The entire class of "no matching PV" and "PV already bound" problems disappears when the cluster provisions volumes on demand. Keep a well-tested, well-documented StorageClass as the default in every cluster, and make sure the CSI driver backing it is healthy — a broken provisioner is often the silent cause behind a wave of Pending PVCs.

    If you're using static PVs, maintain an inventory and run automated checks before deployments go out. A simple pre-deploy validation that queries available PVs at the expected size can catch shortfalls before workloads are affected:

    $ kubectl get pv -o json | jq '[.items[] | select(.status.phase == "Available") | {name: .metadata.name, capacity: .spec.capacity.storage}]'

    Use consistent StorageClass naming across environments. If staging uses a class called

    fast-ssd
    , production should have a class with the same name even if the underlying provisioner differs. This prevents the all-too-common scenario where manifests work perfectly in staging and fail silently in production due to StorageClass name drift.

    For PVs with a Retain reclaim policy, build a process to handle Released volumes. Either automate the

    claimRef
    cleanup after data verification, or switch to the Delete reclaim policy for workloads where preserving data after PVC deletion isn't required.

    Document access mode requirements explicitly in your application manifests and validate them against your storage backend's capabilities during design and review — not after deployment. If a backend doesn't support RWX, catch that at the architecture stage, not at 2am when a deployment fails.

    Finally, set up monitoring on PVC status. A PVC stuck in Pending for more than a few minutes should trigger an alert. With Prometheus and kube-state-metrics, the query is straightforward:

    kube_persistentvolumeclaim_status_phase{phase="Pending"} == 1

    Catching a stuck PVC within minutes of creation is far better than finding out an hour later when a critical workload fails to start. Combined with a clear runbook — like this one — your team can resolve binding failures quickly and consistently without escalating every incident.

    Frequently Asked Questions

    Why is my PVC stuck in Pending even though a PV exists?

    A PVC won't bind to a PV unless all criteria match: StorageClass name, access modes, capacity (PV must be >= PVC request), the PV must be in Available status, and any label selectors on the PVC must match the PV's labels. Use 'kubectl describe pvc <name>' and check the Events section — it will usually tell you exactly which condition is failing.

    How do I rebind a PV that is in Released status?

    Remove the claimRef from the PV using: kubectl patch pv <pv-name> --type json -p '[{"op": "remove", "path": "/spec/claimRef"}]'. This returns the PV to Available status so a new PVC can claim it. Only do this after confirming the previous data is no longer needed or has been backed up.

    What does WaitForFirstConsumer mean for PVC binding?

    When a StorageClass has volumeBindingMode: WaitForFirstConsumer, the PV is not provisioned or bound until a pod referencing the PVC is scheduled. A PVC in Pending with this binding mode is expected behavior — it will bind once a consuming pod is created and scheduled to a node.

    Can a PVC bind to a PV with more storage than requested?

    Yes. A PVC requesting 10Gi can bind to a 20Gi PV, though the extra 10Gi is effectively wasted. However, a PVC requesting 20Gi will not bind to a 10Gi PV — the PV must have capacity greater than or equal to what the PVC requests.

    How do I find which PVC is bound to a specific PV?

    Run 'kubectl describe pv <pv-name>' and look at the Claim field. It will show the namespace and PVC name in the format 'namespace/pvc-name'. You can also use 'kubectl get pv -o wide' for a quick overview of all PV claim relationships.

    Related Articles