CertGrid CertGrid
Hands-on Lab·Certified Kubernetes Administrator

Troubleshooting PVC Pending

Three claims, all Pending, and only the event type tells them apart: one is waiting on purpose, one names a class that does not exist, and one asks for something the backend cannot do and does not say so.

Troubleshooting Guide 101 of 103 Intermediate

Written against the versions above. A Normal event means wait. A Warning event means act. That distinction is the whole diagnosis.

A cluster with one default StorageClass using WaitForFirstConsumer.
Server NameIP AddressOSRolesCPURAMHDD
CKA1001192.168.0.175Ubuntu 26.04 LTSControl Plane Node2 Core4 GB50 GB
CKA1001-NODE01192.168.0.176Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE02192.168.0.177Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE03192.168.0.178Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB

Before you start

  1. Three claims that look identical

    Three PVCs, applied together:

    • wait-consumer - no storageClassName, so it gets the default.
    • no-such-class - names fast-nvme, which does not exist on this cluster.
    • want-rwx - asks for ReadWriteMany from a node-local provisioner that cannot provide it.

    After fifteen seconds all three are Pending. The table gives you nothing to work with: same status, no volume, no capacity. The only visible difference is that one shows fast-nvme in the STORAGECLASS column and the others show local-path, which is the default having been filled in for them.

    That filled-in default is worth noticing on its own. wait-consumer and want-rwx were submitted with no class and now name one, because the API server resolves the default and writes it onto the claim at creation time. Two consequences: changing the cluster default later does not affect existing claims, and a claim created when no default existed keeps an empty class forever rather than picking one up when a default appears.

    bash Example session
    kubectl apply -f - <<'EOF'apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: wait-consumer  namespace: t3spec:  accessModes: [ReadWriteOnce]  resources: {requests: {storage: 100Mi}}---apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: no-such-class  namespace: t3spec:  storageClassName: fast-nvme  accessModes: [ReadWriteOnce]  resources: {requests: {storage: 100Mi}}---apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: want-rwx  namespace: t3spec:  accessModes: [ReadWriteMany]  resources: {requests: {storage: 100Mi}}EOFpersistentvolumeclaim/wait-consumer createdpersistentvolumeclaim/no-such-class createdpersistentvolumeclaim/want-rwx createdsleep 15; kubectl get pvc -n t3NAME            STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGEno-such-class   Pending                                      fast-nvme      <unset>                 15swait-consumer   Pending                                      local-path     <unset>                 15swant-rwx        Pending                                      local-path     <unset>                 15skubectl get pvc -n t3 -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,SC:.spec.storageClassName,MODES:.spec.accessModes --no-headersno-such-class   Pending   fast-nvme    [ReadWriteOnce]wait-consumer   Pending   local-path   [ReadWriteOnce]want-rwx        Pending   local-path   [ReadWriteMany]

    Expected resultThree Pending claims. The custom-columns view adds the access modes, which is the only hint that the third one is different, and it is not enough on its own.

    Success conditionAll three claims are Pending with no volume.

  2. The events, and the Normal-versus-Warning distinction

    Now the events, which is where the difference actually is.

    wait-consumer:

    Normal  WaitForFirstConsumer  waiting for first consumer to be created before binding

    no-such-class:

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

    want-rwx:

    Normal  WaitForFirstConsumer  waiting for first consumer to be created before binding

    The first thing to read is the type, before the message:

    • Normal - nothing is wrong. The controller is waiting for something that has not happened yet.
    • Warning - something failed and will keep failing. Act on it.

    So wait-consumer needs no action at all: create a Pod that mounts it and it binds. This is by far the most common false alarm in Kubernetes storage, and a Normal event is the whole answer.

    no-such-class needs a real fix, and the message names it exactly.

    And then want-rwx, which is the honest and awkward result: its event is identical to wait-consumer's. The access mode is never mentioned. Nothing in the claim's status or events indicates that the request is impossible.

    The reason is volumeBindingMode: WaitForFirstConsumer. Provisioning is deferred until a Pod schedules, and the access mode is only validated when the provisioner is finally asked to act. Until then the controller has nothing to complain about, so it reports the same wait.

    The practical consequence: a WaitForFirstConsumer event does not mean the claim will bind once a Pod arrives. It means nobody has tried yet. To find out, create the Pod and watch what happens next; only then does the provisioner produce a real error.

    This is worth knowing because it is a genuine gap in the feedback. You can write an impossible claim, get a reassuring Normal event, and discover the problem only when a Pod fails to start.

    bash Example session
    kubectl describe pvc wait-consumer -n t3 | tail -3  Type    Reason                Age               From                         Message  ----    ------                ----              ----                         -------  Normal  WaitForFirstConsumer  1s (x2 over 15s)  persistentvolume-controller  waiting for first consumer to be created before bindingkubectl describe pvc no-such-class -n t3 | tail -3  Type     Reason              Age               From                         Message  ----     ------              ----              ----                         -------  Warning  ProvisioningFailed  1s (x2 over 15s)  persistentvolume-controller  storageclass.storage.k8s.io "fast-nvme" not foundkubectl describe pvc want-rwx -n t3 | tail -3  Type    Reason                Age               From                         Message  ----    ------                ----              ----                         -------  Normal  WaitForFirstConsumer  1s (x2 over 15s

    Expected resultOne Warning and two Normals, with the two Normals indistinguishable despite one of them being genuinely impossible. The last line is truncated in the capture output and is the same message as the first.

    Success conditionYou can tell the class error from the waiting claims by event type.

  3. A Pod blocked by its claim

    The consequence of an unbound claim, and it is the form the problem usually reaches you in: nobody notices a Pending PVC, they notice a Pending Pod.

    The scheduler's message is unambiguous:

    0/4 nodes are available: pod has unbound immediate PersistentVolumeClaims.

    The scheduler will not place a Pod whose storage does not exist, because doing so would produce a Pod stuck at ContainerCreating on a node that can never satisfy it. Refusing to schedule is the better failure.

    The last query confirms which kind of Pending this is:

    nodeName='' containerStatuses=

    No node, no container status. As the Pod-stuck-Pending guide sets out, that is the signature of a Pod the scheduler never placed, and it means every query into .status.containerStatuses returns nothing.

    So the diagnostic chain runs Pod Pending -> read the scheduler message -> it names the PVC -> diagnose the PVC. Do not debug the Pod; there is nothing wrong with it.

    A note on the word immediate in that message. It appears even though this claim's class uses WaitForFirstConsumer, because from the scheduler's point of view a claim that has failed provisioning is simply unbound and needed now. Do not read it as a statement about the binding mode.

    And the useful property of WaitForFirstConsumer in this situation: creating the Pod is what makes the provisioner try. For a claim whose real problem is an unsupported access mode, that is the step that finally produces a Warning naming it.

    bash Example session
    kubectl apply -f - <<'EOF'apiVersion: v1kind: Podmetadata:  name: blocked  namespace: t3spec:  volumes:  - name: data    persistentVolumeClaim:      claimName: no-such-class  containers:  - name: app    image: busybox:1.36    command: ["sleep", "3600"]    volumeMounts:    - {name: data, mountPath: /data}    resources: {requests: {cpu: 10m, memory: 16Mi}}EOFpod/blocked createdsleep 15; kubectl get pod blocked -n t3 --no-headers | awk '{print $1, $3}'blocked Pendingkubectl describe pod blocked -n t3 | sed -n '/Events:/,$p' | tail -3  Type     Reason            Age   From               Message  ----     ------            ----  ----               -------  Warning  FailedScheduling  15s   default-scheduler  0/4 nodes are available: pod has unbound immediate PersistentVolumeClaims. not foundkubectl get pod blocked -n t3 -o jsonpath="nodeName='{.spec.nodeName}'{\" containerStatuses=\"}{.status.containerStatuses}{\"\n\"}"nodeName='' containerStatuses=

    Expected resultFailedScheduling naming the unbound claim, and an empty node and container status. The trailing not found on that message is the provisioner's class error surfacing into the scheduler's text.

    Success conditionThe Pod is Pending with unbound immediate PersistentVolumeClaims.

Troubleshooting

Official sources