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
- Kubernetes1.36.4
- Provisionerrancher.io/local-path
- Cluster4 nodes
- Runtimecontainerd 2.2.6
- TimeAbout 25 min
- Reviewed21 August 2026
Written against the versions above. A Normal event means wait. A Warning event means act. That distinction is the whole diagnosis.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE03 | 192.168.0.178 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- The PersistentVolumes and claims guide, and the dynamic provisioning guide.
- The access modes guide, for what a backend can and cannot offer.
- A scratch namespace.
-
Three claims that look identical
Three PVCs, applied together:
wait-consumer- nostorageClassName, so it gets the default.no-such-class- namesfast-nvme, which does not exist on this cluster.want-rwx- asks forReadWriteManyfrom 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 showsfast-nvmein the STORAGECLASS column and the others showlocal-path, which is the default having been filled in for them.That filled-in default is worth noticing on its own.
wait-consumerandwant-rwxwere 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.
-
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 bindingno-such-class:Warning ProvisioningFailed storageclass.storage.k8s.io "fast-nvme" not foundwant-rwx:Normal WaitForFirstConsumer waiting for first consumer to be created before bindingThe 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-consumerneeds 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 aNormalevent is the whole answer.no-such-classneeds a real fix, and the message names it exactly.And then
want-rwx, which is the honest and awkward result: its event is identical towait-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
WaitForFirstConsumerevent 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
Normalevent, 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 15sExpected 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.
-
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
ContainerCreatingon 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.containerStatusesreturns 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
immediatein that message. It appears even though this claim's class usesWaitForFirstConsumer, 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
WaitForFirstConsumerin 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 result
FailedSchedulingnaming the unbound claim, and an empty node and container status. The trailingnot foundon that message is the provisioner's class error surfacing into the scheduler's text.Success conditionThe Pod is Pending with
unbound immediate PersistentVolumeClaims.
Troubleshooting
A PVC is Pending and you want to know whether to act.
Why: The status is the same for a claim that is waiting and one that has failed.
Fix:Read the event type first.
Normal WaitForFirstConsumermeans create a Pod and it will bind.Warning ProvisioningFailedmeans fix what the message names.kubectl describe pvc <name> | tail -5gets you there in one command.A PVC is Pending with no events at all.
Why: No controller has claimed the request. Usually the cluster has no default StorageClass and the claim named none, so nothing is responsible for it.
Fix:
kubectl get scand look for(default). If there is none, either name a class on the claim or annotate one as default. Note that a claim created while no default existed keeps an empty class permanently and will not pick one up later; recreate it.storageclass ... not foundfor a class you can see with kubectl get sc.Why: The name does not match exactly. StorageClass names are case-sensitive and often contain hyphens that get typed as underscores.
Fix:Copy it rather than typing it:
kubectl get sc -o name. Also check you are looking at the same cluster; a manifest written for one environment naming that environment's class is a common cause.A claim waited, a Pod was created, and now the claim reports a provisioner error.
Why: Working as designed.
WaitForFirstConsumerdefers provisioning, so the real error can only appear once something tries.Fix:This is why a
WaitForFirstConsumerevent is not a promise, as step 2 shows. Read the new event on the claim and the provisioner's logs, for examplekubectl logs -n local-path-storage deploy/local-path-provisioner. Unsupported access modes and backend quota errors both surface only at this point.A Pod is Pending and you are debugging the Pod.
Why: The Pod is fine. Its claim is not bound, and the scheduler refuses to place it.
Fix:Read the scheduler's message; it names the cause. Then work on the PVC.
nodeNamebeing empty andcontainerStatusesabsent confirms the Pod never reached a kubelet, so nothing about the container can be at fault yet.A PVC asking for ReadWriteMany never binds and nothing explains why.
Why: The backend cannot provide it, and with
WaitForFirstConsumernothing validates the access mode until a Pod schedules.Fix:Check what your provisioner supports before writing the claim; block and node-local storage are RWO only. To force the error into the open, create a Pod that mounts the claim and read the resulting event. RWX needs a network filesystem class.
A PVC will not delete and sits in Terminating.
Why: The
kubernetes.io/pvc-protectionfinalizer. A claim in use by a Pod cannot be removed.Fix:
kubectl describe pvc <name>showsUsed By. Delete that Pod and the claim finishes by itself. Removing the finalizer by hand leaves a volume attached with no claim describing it, so only do that when the Pod is genuinely gone.