Kubernetes Error Guide: 'node(s) had volume node affinity conflict' — Fix Pod Scheduling
Fix the 'node(s) had volume node affinity conflict' scheduling failure in Kubernetes: zonal PVs, topology-bound StorageClasses, cordoned zones, and pods pinned away from their volume.
- #kubernetes
- #troubleshooting
- #errors
- #storage
Overview
volume node affinity conflict is a scheduling failure: the pod needs a PersistentVolume that is pinned to a specific topology (usually one availability zone or one node), and no schedulable node in that topology can also satisfy the pod’s other constraints. The scheduler cannot place the pod on any node that can both attach the volume and meet its requirements, so the pod stays Pending.
You will see it in the pod’s events:
Warning FailedScheduling 0/6 nodes are available: 3 node(s) had volume node affinity conflict,
3 node(s) didn't match Pod's node affinity/selector. preemption: 0/6 nodes are available.
The PV carries a nodeAffinity block (set at provisioning time by the CSI driver or the admin) that says “this volume lives in zone us-east-1a.” If every node in us-east-1a is cordoned, full, tainted, or excluded by the pod’s own affinity, the volume becomes unreachable and the pod cannot schedule. This is most common with zonal block storage (EBS, GCE PD, Azure Disk) and local PVs.
Symptoms
- Pod is stuck
Pending;kubectl describe podshowsFailedSchedulingmentioningvolume node affinity conflict. - A previously-running StatefulSet pod will not reschedule after a node drain or zone outage.
- The count of conflicting nodes matches the nodes outside the volume’s zone.
- The PV shows a
nodeAffinityrequirement pinned to one zone or one node.
kubectl get pod postgres-0 -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE
postgres-0 0/1 Pending 0 6m <none> <none> <none>
kubectl describe pod postgres-0 | grep -A3 Events
Events:
Type Reason Age From Message
Warning FailedScheduling 5m (x8 over 6m) default-scheduler 0/6 nodes are available:
3 node(s) had volume node affinity conflict, 3 node(s) didn't match node selector.
Common Root Causes
1. Zonal PV, but the pod’s zone has no schedulable nodes
The PV is bound to, say, us-east-1a, but every node in 1a is cordoned, drained, or was removed by the autoscaler. The volume can only attach in 1a, so the pod cannot run anywhere.
kubectl get pv $(kubectl get pvc data-postgres-0 -o jsonpath='{.spec.volumeName}') \
-o jsonpath='{.spec.nodeAffinity}{"\n"}'
{"required":{"nodeSelectorTerms":[{"matchExpressions":[
{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}
kubectl get nodes -l topology.kubernetes.io/zone=us-east-1a
No resources found
The zone has zero nodes. Uncordon/scale a node in 1a, or restore capacity there.
2. Pod affinity or nodeSelector fights the volume’s zone
The pod (or its controller) has a nodeSelector/nodeAffinity that pins it to a different zone than the PV. Each is satisfiable alone, but not together.
kubectl get pod postgres-0 -o jsonpath='{.spec.nodeSelector}{"\n"}'
{"topology.kubernetes.io/zone":"us-east-1b"}
The pod wants 1b; the PV lives in 1a. Remove the conflicting selector or provision the volume in the pod’s target zone.
3. StorageClass without WaitForFirstConsumer provisioned the PV in the wrong zone
With volumeBindingMode: Immediate, the volume is provisioned before the pod is scheduled, so the CSI driver picks a zone blindly — often not where the pod can run. WaitForFirstConsumer defers binding until the scheduler picks a node, avoiding the mismatch.
kubectl get storageclass gp3 -o jsonpath='{.volumeBindingMode}{"\n"}'
Immediate
4. Local PV whose node is gone or cordoned
A local PersistentVolume is pinned to exactly one node. If that node is drained, tainted, or deleted, the data is unreachable and any pod bound to it cannot schedule.
kubectl get pv local-pv-abc -o jsonpath='{.spec.nodeAffinity}{"\n"}'
kubectl get node worker-3 -o jsonpath='{.spec.taints}{"\n"}'
Diagnostic Workflow
Step 1: Confirm the pod is Pending on this exact reason
kubectl describe pod <POD> | grep -A6 Events
Look for volume node affinity conflict specifically — a generic FailedScheduling about CPU/taints is a different problem.
Step 2: Find the PVC → PV → topology chain
PVC=$(kubectl get pod <POD> -o jsonpath='{.spec.volumes[?(@.persistentVolumeClaim)].persistentVolumeClaim.claimName}')
PV=$(kubectl get pvc "$PVC" -o jsonpath='{.spec.volumeName}')
kubectl get pv "$PV" -o jsonpath='{.spec.nodeAffinity}{"\n"}'
This prints the exact zone/node the volume is pinned to.
Step 3: Check whether any schedulable node matches that topology
# Substitute the zone from Step 2
kubectl get nodes -l topology.kubernetes.io/zone=<ZONE> -o wide
kubectl get nodes -l topology.kubernetes.io/zone=<ZONE> \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.unschedulable}{"\n"}{end}'
Zero nodes, or all unschedulable: true, is the conflict.
Step 4: Compare against the pod’s own placement constraints
kubectl get pod <POD> -o jsonpath='{.spec.nodeSelector}{"\n"}'
kubectl get pod <POD> -o jsonpath='{.spec.affinity}{"\n"}'
If the pod is pinned to a different zone than the PV, that is the conflict.
Step 5: Check the StorageClass binding mode
SC=$(kubectl get pvc "$PVC" -o jsonpath='{.spec.storageClassName}')
kubectl get storageclass "$SC" -o jsonpath='{.volumeBindingMode}{"\n"}'
Immediate on a multi-zone cluster is a recurring cause; WaitForFirstConsumer is the fix for new volumes.
Example Root Cause Analysis
A StatefulSet pod redis-0 won’t come back after routine node maintenance.
kubectl describe pod redis-0 | grep -A4 Events
Warning FailedScheduling 2m (x15) default-scheduler 0/9 nodes are available:
6 node(s) didn't match pod affinity, 3 node(s) had volume node affinity conflict.
Trace the volume’s zone:
kubectl get pv $(kubectl get pvc data-redis-0 -o jsonpath='{.spec.volumeName}') \
-o jsonpath='{.spec.nodeAffinity}{"\n"}'
...topology.kubernetes.io/zone In [us-west-2c]...
Check nodes in us-west-2c:
kubectl get nodes -l topology.kubernetes.io/zone=us-west-2c \
-o custom-columns=NAME:.metadata.name,SCHED:.spec.unschedulable
NAME SCHED
ip-10-2-3-4 true
The only node in 2c was cordoned during maintenance and never uncordoned. The zonal EBS volume can attach only in 2c, so redis-0 is stuck. Fix:
kubectl uncordon ip-10-2-3-4
Within a few seconds the scheduler places redis-0 on that node, the volume attaches, and the pod goes Running. The durable fix is to ensure each zone that holds stateful volumes always retains schedulable capacity, and to run enough replicas across zones that one cordoned node is not a single point of failure.
Prevention Best Practices
- Use
volumeBindingMode: WaitForFirstConsumeron multi-zone clusters so volumes are provisioned in the zone where the pod actually schedules — this eliminates the most common cause for new PVCs. - Keep schedulable capacity in every zone that holds zonal or local PVs; never drain the last node of a zone that pins stateful data.
- For StatefulSets, spread replicas across zones (topology spread constraints) so a single zone’s outage cannot strand the whole set.
- Avoid pinning pods with
nodeSelectorto a zone different from where their volumes live; let the volume topology drive placement. - For local PVs, treat the owning node as part of the data’s durability story — its loss means the data is gone, not just delayed.
- Watch autoscaler behavior: a scale-in that removes the last node in a zone can strand volumes there. See more in Kubernetes & Helm guides.
Quick Command Reference
# Confirm the reason
kubectl describe pod <POD> | grep -A6 Events
# PVC -> PV -> pinned topology
PVC=$(kubectl get pod <POD> -o jsonpath='{.spec.volumes[?(@.persistentVolumeClaim)].persistentVolumeClaim.claimName}')
PV=$(kubectl get pvc "$PVC" -o jsonpath='{.spec.volumeName}')
kubectl get pv "$PV" -o jsonpath='{.spec.nodeAffinity}{"\n"}'
# Schedulable nodes in the volume's zone
kubectl get nodes -l topology.kubernetes.io/zone=<ZONE> \
-o custom-columns=NAME:.metadata.name,SCHED:.spec.unschedulable
# Pod's own placement constraints
kubectl get pod <POD> -o jsonpath='{.spec.nodeSelector} {.spec.affinity}{"\n"}'
# StorageClass binding mode
kubectl get storageclass <SC> -o jsonpath='{.volumeBindingMode}{"\n"}'
# Common fixes
kubectl uncordon <NODE> # restore capacity in the zone
# or provision new volumes with WaitForFirstConsumer StorageClass
Conclusion
node(s) had volume node affinity conflict means the scheduler cannot find a node that both satisfies the pod’s constraints and can attach a topology-pinned volume. The usual root causes:
- The PV’s zone has no schedulable nodes (cordoned, drained, or scaled away).
- The pod’s own
nodeSelector/affinity pins it to a different zone than the volume. volumeBindingMode: Immediateprovisioned the PV in the wrong zone before scheduling.- A local PV’s owning node is gone, tainted, or unschedulable.
Trace PVC → PV → nodeAffinity to learn the volume’s zone, then check for schedulable nodes there. The lasting fix is WaitForFirstConsumer plus keeping capacity in every zone that holds stateful data. For ad-hoc triage, the free incident assistant can turn a scheduling describe dump into the likely root cause.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.