Troubleshooting Pod Pending
The scheduler explains itself precisely and almost nobody reads it. Two unschedulable Pods, two different tallies, and the reason every container-status query returns nothing for either of them.
Troubleshooting Guide 80 of 103 Beginner
- Kubernetes1.36.4
- Cluster4 nodes
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- TimeAbout 25 min
- Reviewed21 August 2026
Written against the versions above. Node counts in the message are this lab's four. The format of the tally is what to learn.
| 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 requests, limits and QoS guide, since scheduling decisions are made on requests.
- The taints and tolerations guide, and the labels and nodeSelector guide: both appear in the messages below.
- A scratch namespace.
-
Not enough room, and the tally that says so
A Pod requesting 8 CPUs and 32Gi on a cluster of 2-core nodes. It stays
Pending, and the scheduler explains exactly why:0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 Insufficient cpu, 3 Insufficient memory.That sentence is a tally, and reading it properly is the whole skill. It says: four nodes considered, none usable; one rejected on a taint, three rejected on CPU and the same three on memory.
Two things to take from the format:
- The counts should add up to the total, allowing for a node being rejected on several grounds. Here 1 tainted plus 3 short of resources covers all four.
- The reasons are ranked by frequency, not by importance, so the first one listed is not necessarily your problem.
1 node(s) had untolerated taint(s)is the control plane'sNoScheduletaint doing its job; it appears in almost every unschedulable message on a kubeadm cluster and is usually noise.
The real cause here is
3 Insufficient cpu, confirmed by the allocatable figures: every node has 2 cores, and the Pod wants 8. No amount of waiting fixes that.The trailing clause matters too:
preemption: 0/4 nodes are available: 4 Preemption is not helpful. The scheduler considered evicting lower-priority Pods to make room and concluded it would not help, which is correct when the request exceeds the node's total capacity. SeeingPreemption is not helpfultells you the request is beyond any single node, not merely beyond what is free right now.A note on where to read this. The
Unschedulablecondition holds the same text as the event, and it persists, while events expire after an hour. On a Pod that has been Pending since yesterday the event list is empty and the condition still tells you why.bash Example session kubectl apply -f - <<'EOF'apiVersion: v1kind: Podmetadata: name: toobig namespace: tshspec: containers: - name: app image: nginx:1.29-alpine resources: requests: {cpu: "8", memory: 32Gi}EOFpod/toobig createdsleep 15; kubectl get pod toobig -n tshNAME READY STATUS RESTARTS AGEtoobig 0/1 Pending 0 15skubectl get pod toobig -n tsh -o jsonpath="{range .status.conditions[*]}{.type}={.status}{\" \"}{.reason}{\": \"}{.message}{\"\n\"}{end}"PodScheduled=False Unschedulable: 0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 Insufficient cpu, 3 Insufficient memory. no new claims to deallocate, preemption: 0/4 nodes are available: 4 Preemption is not helpful for scheduling.kubectl describe pod toobig -n tsh | sed -n '/Events:/,$p' | tail -4Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 15s default-scheduler 0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 Insufficient cpu, 3 Insufficient memory. no new claims to deallocate, preemption: 0/4 nodes are available: 4 Preemption is not helpful for scheduling.kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU:.status.allocatable.cpu,MEM:.status.allocatable.memory --no-headerscka1001 2 3377972Kicka1001-node01 2 3377980Kicka1001-node02 2 3377980Kicka1001-node03 2 3377980KiExpected resultThe same message from both the condition and the event.
allocatablerather thancapacityis the right column to compare against: it is capacity minus what the kubelet reserves for the system, so it is what the scheduler actually has to spend.Success conditionThe
Unschedulablemessage namesInsufficient cpu. -
A label nothing has, and a different tally
Same Pending status, tiny resource request, completely different cause. This Pod asks for
disktype: nvme-that-does-not-exist.0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 node(s) didn't match Pod's node affinity/selector.The taint clause is identical noise. The informative half changed to
didn't match Pod's node affinity/selector, which coversnodeSelector,nodeAffinityandnodeNametogether, so the message does not tell you which of the three. Read the spec to find out.So a short field guide to the common clauses:
Insufficient cpu/Insufficient memory- requests exceed what is free. Compare againstallocatable.didn't match Pod's node affinity/selector- a label requirement nothing satisfies. Check withkubectl get nodes -l; no output means no candidate.had untolerated taint(s)- usually the control plane, and usually irrelevant. It becomes the real cause when it accounts for all nodes.node(s) had volume node affinity conflict- the Pod's PersistentVolume is node-local and that node is not usable.didn't match pod anti-affinity rules- the Pod refuses to sit with something already there.node(s) were unschedulable- nodes are cordoned.kubectl get nodesshowsSchedulingDisabled.
The method in all cases is the same: subtract the noise, then check whether the remaining reason accounts for every node. If it does, that is your answer.
bash Example session kubectl apply -f - <<'EOF'apiVersion: v1kind: Podmetadata: name: nonode namespace: tshspec: nodeSelector: disktype: nvme-that-does-not-exist containers: - name: app image: nginx:1.29-alpine resources: {requests: {cpu: 10m, memory: 16Mi}}EOFpod/nonode createdsleep 12; kubectl get pod nonode -n tsh --no-headers | awk '{print $1, $3}'nonode Pendingkubectl describe pod nonode -n tsh | sed -n '/Events:/,$p' | tail -3 Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 12s default-scheduler 0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 node(s) didn't match Pod's node affinity/selector. no new claims to deallocate, preemption: 0/4 nodes are available: 4 Preemption is not helpful for scheduling.Expected resultA 10m CPU request rejected, which rules resources out immediately and shows that Pending says nothing about size on its own.
Success conditionThe message names an affinity or selector mismatch.
-
Why every container query comes back empty
Now the detail that wastes the most time, and it is visible in the comparison table.
nonode Pending <none> <none> <none> toobig Pending <none> <none> <none>Every column reads
, including the restart count. Comparebadimage, alsoPending, which has a real waiting reason.The difference is that an unscheduled Pod has no
containerStatusesat all. That array is populated by the kubelet, and no kubelet owns this Pod yet, because it has no node. So there is nothing to report and nothing to read.That divides Pending into two genuinely different situations:
- Pending, no container status - the scheduler could not place it. Look at
.status.conditionsandFailedSchedulingevents. - Pending, with a container status - it was placed and the kubelet cannot start it. Image pull, volume mount, or a config reference that does not resolve.
kubectl get podsettles it in one command: empty means unscheduled.-o jsonpath='{.spec.nodeName}' The practical consequence for tooling: any script that inspects
.status.containerStatuses[0]silently returns nothing for every unschedulable Pod in the cluster. A dashboard built that way shows those Pods as having no problem at all.One last thing the event list shows, and it is a small relief: there is exactly one
FailedSchedulingevent per Pod rather than a stream. The scheduler retries with back-off and updates the existing event's count rather than emitting new ones, so a Pending Pod does not flood the event log. It also means theAgeon that event is when it was last retried, not when the trouble started.bash Example session kubectl get pods -n tsh -o custom-columns=NAME:.metadata.name,PHASE:.status.phase,REASON:.status.containerStatuses[0].state.waiting.reason,TERM:.status.containerStatuses[0].state.terminated.reason,RESTARTS:.status.containerStatuses[0].restartCount --no-headersbadimage Pending ImagePullBackOff <none> 0crasher Running <none> Error 4hungry Failed <none> OOMKilled 0noauth Pending ImagePullBackOff <none> 0nonode Pending <none> <none> <none>toobig Pending <none> <none> <none>kubectl get events -n tsh --sort-by=.lastTimestamp -o custom-columns=OBJ:.involvedObject.name,TYPE:.type,REASON:.reason --no-headers | tail -12noauth Warning Failednoauth Normal Pullingtoobig Warning FailedSchedulingnonode Warning FailedSchedulingbadimage Warning Failedbadimage Normal BackOffnoauth Normal BackOffnoauth Warning Failedcrasher Warning BackOffcrasher Normal Startedcrasher Normal Createdcrasher Normal PulledExpected resultTwo Pending rows with nothing in them and two with a reason. The absence of
Scheduledevents fortoobigandnonode, where every other Pod has one, is the same fact from the event side.Success conditionYou can tell an unscheduled Pod from an unstarted one by whether container status exists.
- Pending, no container status - the scheduler could not place it. Look at
Troubleshooting
A Pod is Pending and
kubectl describeshows no events.Why: Events expire, by default after an hour. A Pod Pending since yesterday has none left.
Fix:Read the condition instead, which persists:
kubectl get pod <name> -o jsonpath='{.status.conditions}'. It carries the sameUnschedulablemessage. This is the reason to prefer the condition over the event as a first command.The message blames a taint and you cannot see which.
Why: The tally counts nodes without naming taints.
Fix:List them:
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints. On a kubeadm cluster1 node(s) had untolerated taint(s)is the control plane and is usually not your problem; it matters when the count covers every node, which means something has tainted the workers, often aNotReadyor pressure taint added automatically.Insufficient cpubut the nodes look mostly idle.Why: Scheduling is done on requests, not on usage. A node whose Pods request all its CPU is full even at 5% actual utilisation.
Fix:Compare requests, not
kubectl top:kubectl describe node <name>prints anAllocated resourcestable with request totals and percentages. The usual finding is a Pod with a large request it never uses. Right-size the requests rather than adding nodes.A Pod with a nodeSelector will not schedule and you believe the label is set.
Why: A typo, or the label is on the wrong nodes, or set with a different value.
Fix:Ask the cluster the same question the scheduler did:
kubectl get nodes -l disktype=ssd. No output means no candidate, and that is conclusive.kubectl get nodes --show-labelsshows what is actually there. Remember label values are case-sensitive.Pending with
volume node affinity conflict.Why: The Pod's PVC is bound to a node-local volume and that node cannot take the Pod.
Fix:Find the node the volume is on:
kubectl get pv $(kubectl get pvc <name> -o jsonpath='{.spec.volumeName}') -o jsonpath='{.spec.nodeAffinity}'. Then work out why that node is unusable, usually cordoned or full. Covered in the dynamic provisioning guide.Pods stay Pending and there are no
FailedSchedulingevents at all.Why: Nothing is scheduling. The scheduler is down, or running without its leader lease.
Fix:Different from an unschedulable Pod, which gets an explanation. No explanation means no scheduler.
kubectl get pods -n kube-system -l component=kube-schedulerandkubectl get lease -n kube-system kube-scheduler -o yaml, checkingrenewTimeis current. A scheduler Pod can be Running while not holding the lease and doing no work.A Pending Pod has a
Scheduledevent but never starts.Why: It is not a scheduling problem. It has a node, and the kubelet cannot start it.
Fix:Switch to container status:
kubectl get pod <name> -o jsonpath='{.status.containerStatuses[0].state}'. Image pulls, mount failures and missing ConfigMap or Secret references all look like Pending from the outside and are diagnosed entirely differently.