A pod sits Pending and the scheduler quietly logs why it rejected every node. Learn to read that one line and fix the real cause instead of guessing.
A FailedScheduling event means the scheduler did its job and came up empty. It walked every node in the cluster, ran each one through a set of predicates, and not a single node passed all of them. The pod stays Pending. Nothing is broken; the cluster is telling you, in one dense line, that what you asked for and what you have don't line up.
The mistake I see people make is scaling the node group the moment they see Pending. Sometimes that's right. Often it's not, and you've just paid for capacity that won't fix a taint or an affinity typo. Read the event first.
kubectl describe pod api-7f9c8-2xk4p
Scroll to the bottom:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 47s default-scheduler 0/5 nodes are available: 3 Insufficient cpu, 2 node(s) had untolerated taint {dedicated: gpu}. preemption: 0/5 nodes are available: 3 No preemption victims found, 2 Preemption is not helpful for scheduling.
That message is a manifest. 0/5 nodes are available is the headline. The comma-separated list is a tally of why each node failed, and the counts add up to your node total. Here: three nodes had no CPU room, two carried a taint the pod didn't tolerate. Five nodes, zero fits.
Learn to parse each phrase. Below are the ones you'll actually meet.
0/5 nodes are available: 5 Insufficient cpu.
The pod's total CPU requests (not limits, requests) exceed what's free on every node. Free means allocatable minus what's already reserved by other pods, whether or not they're using it. A node can sit at 10% real utilization and still reject a pod because everything on it over-requested.
Two fixes. If the request is honest, add capacity. If it's inflated — and a lot of resources.requests are copy-pasted guesses — lower it:
resources:
requests:
cpu: "250m" # was "2" because nobody measured
memory: "256Mi"
Check what's actually free before you decide:
kubectl describe node ip-10-0-1-14 | grep -A5 "Allocated resources"
This is the same territory as a pod Pending that never gets placed — resource math is the most common root cause.
2 node(s) had untolerated taint {dedicated: gpu: NoSchedule}.
Someone tainted those nodes to keep general workloads off them. Your pod isn't allowed there unless it carries a matching toleration. If this pod should run there:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
If it shouldn't, the event is doing its job — those nodes are reserved and the pod belongs elsewhere. A toleration only grants permission; it doesn't pull the pod onto the node. That's affinity's job.
0/5 nodes are available: 5 node(s) didn't match Pod's node affinity/selector.
You asked for nodes with a label none of them carry. Ninety percent of the time it's a typo or a zone that doesn't exist. Print the labels:
kubectl get nodes -L topology.kubernetes.io/zone,node.kubernetes.io/instance-type
Then reconcile your selector with reality:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node.kubernetes.io/instance-type
operator: In
values: ["m6i.large", "m6i.xlarge"]
Use required when the pod genuinely cannot run elsewhere. Reach for preferredDuringScheduling when it's a nudge, not a law — required plus a label typo is how you strand a pod forever.
0/5 nodes are available: 5 node(s) didn't satisfy existing pods anti-affinity rules.
Common with a StatefulSet that spreads replicas one-per-node using requiredDuringScheduling anti-affinity. Ask for four replicas on a three-node cluster and the fourth has nowhere legal to sit. Either add a node or loosen the rule to preferred:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: cache
topologyKey: kubernetes.io/hostname
preferred still spreads pods when it can, but it won't wedge the deploy when it can't.
0/3 nodes are available: 3 node(s) had volume node affinity conflict.
This one trips everybody. An EBS volume lives in one availability zone. A pod bound to that PVC can only schedule onto nodes in the same zone. If your only free capacity is in us-east-1b and the volume is in us-east-1a, no dice. Check the PV:
kubectl get pv <name> -o jsonpath='{.spec.nodeAffinity}'
The durable fix is a WaitForFirstConsumer StorageClass so the volume is provisioned after the scheduler picks a zone, not before:
volumeBindingMode: WaitForFirstConsumer
0/5 nodes are available: 2 node(s) were unschedulable, 3 Insufficient cpu.
unschedulable means someone ran kubectl cordon (or a drain in progress) and those nodes won't accept new pods. Sometimes that's a forgotten cordon from last week's maintenance. Confirm and uncordon:
kubectl get nodes | grep SchedulingDisabled
kubectl uncordon ip-10-0-1-22
0/6 nodes are available: 3 node(s) didn't match pod topology spread constraints.
You've asked the scheduler to balance replicas across zones or nodes within a maxSkew, and honoring it would break the limit. With whenUnsatisfiable: DoNotSchedule, the pod waits. If even spread matters less than getting the pod running, switch to ScheduleAnyway:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api
Read the tally before you touch anything. The count after each phrase tells you how many nodes failed for that reason, and that alone usually points at the fix: taints and affinity are almost always a manifest problem you solve in the YAML, while Insufficient cpu split evenly across every node is the one case where scaling is honest. Fix inflated requests before you add nodes — measured requests fix more Pending pods than a bigger cluster ever will. And keep required rules for things that are genuinely required; everything softer belongs in preferred, where a bad label can't strand a workload. For the wider map of failure modes, our Kubernetes troubleshooting guide connects the events that tend to show up together.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Both promise to find your slow query at 3am. One bills by data ingested, the other by host-hour. Here's how that shakes out in a real ops budget.
A shared API key between two internal services proves nothing about who is calling. mTLS makes every service present a cryptographic identity instead.
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.