Back to articles

Pending Pods: A Decision Tree for the Five Most Common Causes

A systematic approach to diagnosing pending pods. Rather than randomly checking configurations, follow a decision tree that addresses the five most common causes in order of likelihood.

You deploy a workload. The pod sits in Pending state. Minutes pass. Nothing happens.

This scenario plays out daily in production clusters. The frustrating part is not that pods get stuck—it is that Kubernetes often provides cryptic hints about why. The scheduler knows exactly what is wrong, but surfacing that information requires knowing where to look.

This article presents a systematic approach to diagnosing pending pods. Rather than randomly checking configurations, we will follow a decision tree that addresses the five most common causes in order of likelihood and diagnostic efficiency.

The Mental Model

The scheduler answers one question for every pod: which node satisfies all constraints simultaneously?

Those constraints include resource requests, node selectors, affinities, tolerations, topology spread, and volume requirements. The scheduler evaluates every node against every constraint. A node must pass all of them to be eligible.

Pending means: no node satisfies all constraints at the same time.

This is the key insight. A pending pod is not a mystery—it is the scheduler telling you that some constraint cannot be satisfied given current cluster state. Your job is to identify which constraint is blocking and either relax it or change the cluster to satisfy it.

The scheduler is deterministic. Same pod spec plus same cluster state equals same scheduling decision. If a pod cannot schedule, randomly deleting and recreating it will not help. You must address the underlying constraint.

The First Command: Always Start Here

Before diving into specific causes, run this:

kubectl describe pod <pod-name> -n <namespace>

Scroll to the Events section at the bottom. Look for events with Type: Warning and Reason: FailedScheduling. The Message field contains the scheduler's explanation.

A typical FailedScheduling event looks like this:

Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  12s   default-scheduler  0/5 nodes are available:
           2 Insufficient cpu, 2 Insufficient memory, 1 node(s) had taint
           {node-role.kubernetes.io/control-plane: }, that the pod didn't tolerate.

This message tells you how many nodes failed each constraint. Parse it carefully—the scheduler aggregates why nodes were rejected and reports counts per reason, not a per-node breakdown.

A note on terminology: Pending means the pod object exists but has not been assigned to a node yet. This is distinct from pods that fail to start after scheduling (like ImagePullBackOff or CrashLoopBackOff), or pods that were never created at all due to admission rejection.

With that output in hand, use the decision tree below.


The Decision Tree

Pending pod decision tree Start with kubectl describe pod and read the FailedScheduling message. If it mentions insufficient cpu or memory, the cause is a resource shortage. If it mentions a node affinity or selector mismatch, the cause is an affinity mismatch. If it mentions a taint that the pod did not tolerate, the cause is a taint or toleration issue. If it mentions an unbound persistentvolumeclaim, the cause is PVC binding. If it mentions an exceeded quota, the cause is a ResourceQuota. Otherwise, look at less common causes. NO NO NO NO NO YES YES YES YES YES Pod stuck in Pending kubectl describe pod → Events Look for FailedScheduling Message mentions "Insufficient cpu / memory"? Cause 1 Resource shortage Message mentions "didn't match node affinity / selector"? Cause 2 Affinity mismatch Message mentions "had taint … didn't tolerate"? Cause 3 Taint / toleration Message mentions "persistentvolumeclaim" / "unbound"? Cause 4 PVC binding Message mentions "forbidden: exceeded quota"? Cause 5 ResourceQuota Less common cause

Now let us examine each cause in detail.


Cause 1: Insufficient Resources (CPU/Memory)

The symptom:

0/5 nodes are available: 5 Insufficient cpu.

Or:

0/5 nodes are available: 3 Insufficient memory, 2 Insufficient cpu.

What is happening:

The scheduler sums the resource requests of all pods already scheduled on each node and compares against allocatable capacity. If your pod's requests would exceed what remains, the node is rejected.

Note

Kubernetes schedules based on requests, not limits and not actual usage. A node can be 5% utilized but "full" from the scheduler's perspective if requests are allocated.

Diagnostic commands:

Check what the pod is requesting:

kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[*].resources.requests}'

Look for the "Allocated resources" section in node output (format varies by version):

kubectl describe node <node-name>

Scroll to "Allocated resources" to see requests versus capacity.

For a more precise view:

kubectl get nodes -o custom-columns=\
"NAME:.metadata.name,\
CPU_ALLOC:.status.allocatable.cpu,\
MEM_ALLOC:.status.allocatable.memory"

Then check what is already scheduled:

kubectl describe node <node-name> | grep -A 20 "Non-terminated Pods"

Remediation options:

  1. Reduce the pod's resource requests if they are overprovisioned. Many teams copy-paste requests without measuring actual usage.
  2. Add nodes to the cluster if workloads genuinely need the resources.
  3. Evict lower-priority workloads using PriorityClasses. Higher-priority pods can preempt lower-priority ones.
  4. Enable the cluster autoscaler if running in a cloud environment. It provisions nodes when pending pods cannot be scheduled due to resource constraints.
  5. Check for resource hoarding. Look for pods with high requests but low actual usage:
kubectl top pods -n <namespace> --sort-by=cpu

Also consider DaemonSets—they consume resources on every node and reduce available capacity for regular pod scheduling. A cluster with many DaemonSets has less headroom than node capacity suggests.


Cause 2: Node Selectors and Affinity Not Matching

The symptom:

0/5 nodes are available: 5 node(s) didn't match Pod's node affinity/selector.

What is happening:

The pod specifies constraints about which nodes it can run on—either via nodeSelector or nodeAffinity—and no nodes satisfy those constraints.

An important distinction: requiredDuringSchedulingIgnoredDuringExecution is a hard constraint that blocks scheduling entirely, while preferredDuringSchedulingIgnoredDuringExecution is a soft preference that influences but does not prevent placement. If you see this error, you have a required constraint that cannot be satisfied.

Diagnostic commands:

Check what the pod requires:

kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.nodeSelector}'
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.affinity.nodeAffinity}'

List node labels to see what is available:

kubectl get nodes --show-labels

For a specific label:

kubectl get nodes -l <label-key>=<label-value>

Common scenarios:

Remediation options:

  1. Add the required label to appropriate nodes:
kubectl label node <node-name> disktype=ssd
  1. Change the pod's selector to match existing labels.
  2. Use preferred affinity instead of required. preferredDuringSchedulingIgnoredDuringExecution allows scheduling on non-matching nodes if necessary:
affinity:
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      preference:
        matchExpressions:
        - key: disktype
          operator: In
          values:
          - ssd

Cause 3: Taints Without Tolerations

The symptom:

0/3 nodes are available: 3 node(s) had taint {dedicated: gpu}, that the pod didn't tolerate.

What is happening:

Taints repel pods. Nodes can be tainted to reserve them for specific workloads. Unless a pod has a matching toleration, it cannot schedule on a tainted node.

Diagnostic commands:

Check taints on all nodes:

kubectl get nodes -o custom-columns=\
"NAME:.metadata.name,\
TAINTS:.spec.taints[*].key"

For full taint details:

kubectl describe nodes | grep -A 3 "Taints:"

Check if the pod has tolerations:

kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.tolerations}'

Common scenarios:

Remediation options:

  1. Add tolerations to the pod spec:
tolerations:
- key: "dedicated"
  operator: "Equal"
  value: "gpu"
  effect: "NoSchedule"
  1. Remove the taint from nodes if it was applied incorrectly:
kubectl taint nodes <node-name> dedicated:NoSchedule-
  1. Check if all nodes are tainted. This sometimes happens after cluster upgrades or when automation misconfigures nodes.

Cause 4: PVC Binding Failures

The symptom:

0/5 nodes are available: 5 node(s) didn't find available persistent volumes to bind.

Or the pod describe shows:

Warning  FailedScheduling  pod has unbound immediate PersistentVolumeClaims

What is happening:

The pod references a PersistentVolumeClaim that is not bound to a PersistentVolume. The scheduler will not place the pod until storage is available.

Diagnostic commands:

Check the PVC status:

kubectl get pvc -n <namespace>

Look for PVCs in Pending state. Then describe the problematic PVC:

kubectl describe pvc <pvc-name> -n <namespace>

The events section reveals why binding failed.

Check available PVs:

kubectl get pv

Check the StorageClass:

kubectl get storageclass

Common scenarios:

Remediation options:

  1. Check the provisioner pods:
kubectl get pods -n kube-system | grep -i csi
kubectl get pods -n kube-system | grep -i provisioner
  1. Verify the StorageClass exists and is default:
kubectl get storageclass

Look for (default) annotation.

  1. For zone issues, either pre-provision a PV in the correct zone or use a StorageClass with zone-aware provisioning.
  2. Check cloud provider quotas if using managed Kubernetes.

Cause 5: ResourceQuota Exceeded

The symptom:

Sometimes this appears in pod events:

Error creating: pods "nginx-xyz" is forbidden: exceeded quota: compute-quota,
requested: cpu=500m, used: cpu=1900m, limited: cpu=2

More commonly, the pod never gets created at all—the ReplicaSet or Deployment shows the error instead.

What is happening:

The namespace has a ResourceQuota, and creating the pod would exceed it. This is an admission-time rejection, not a scheduling failure—the pod object may not even exist.

Diagnostic commands:

Check quotas in the namespace:

kubectl get resourcequota -n <namespace>
kubectl describe resourcequota -n <namespace>

Output shows Used versus Hard limits for each resource type.

Check events on the parent controller:

kubectl describe deployment <deployment-name> -n <namespace>
kubectl describe replicaset <rs-name> -n <namespace>

Common scenarios:

Remediation options:

  1. Increase the quota if authorized:
kubectl patch resourcequota <quota-name> -n <namespace> --type='json' \
  -p='[{"op": "replace", "path": "/spec/hard/cpu", "value": "4"}]'
  1. Reduce resource requests on pods to fit within quota.
  2. Scale down other workloads in the namespace to free quota headroom.
  3. Review quota events to understand historical rejections:
kubectl get events -n <namespace> --field-selector reason=FailedCreate

Less Common Causes

If none of the five causes above apply, consider these:

Pod topology spread constraints: Strict constraints like whenUnsatisfiable: DoNotSchedule with maxSkew: 1 can prevent scheduling if placement would violate the spread.

kubectl get pod <pod-name> -o jsonpath='{.spec.topologySpreadConstraints}'

Pod disruption budgets during rollout: PDBs do not block scheduling directly, but they can prevent old pods from being evicted. During rollouts with constrained resources, this blocks progress and keeps new pods pending while waiting for capacity.

kubectl get pdb -n <namespace>

Scheduler not running: Rare, but verify:

kubectl get pods -n kube-system -l component=kube-scheduler

Admission webhooks rejecting pods: Check for validating or mutating webhooks that might be blocking:

kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations

A Real Debugging Session

Theory is useful, but pressure reveals gaps. Here is an actual debugging flow.

The situation: A team deploys a new service. Pods sit in Pending for ten minutes. The on-call engineer gets paged.

Step 1: Get the scheduler's message.

$ kubectl describe pod api-server-7f8b9c-x4k2m -n production
...
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  2m    default-scheduler  0/8 nodes are available:
           3 Insufficient cpu, 3 Insufficient memory,
           2 node(s) had taint {dedicated: gpu}, that the pod didn't tolerate.

The message reveals: three nodes lack CPU, three lack memory, two are GPU-dedicated. No node passes all constraints.

Step 2: Check the pod's requests.

$ kubectl get pod api-server-7f8b9c-x4k2m -n production \
    -o jsonpath='{.spec.containers[*].resources.requests}'
{"cpu":"2","memory":"4Gi"}

Requesting 2 CPUs and 4Gi memory per pod. Is that reasonable?

Step 3: Check node capacity.

$ kubectl describe node worker-03 | grep -A 8 "Allocated resources"
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests      Limits
  --------           --------      ------
  cpu                3800m (95%)   8000m (200%)
  memory             14Gi (93%)    20Gi (125%)

Worker-03 has 3800m of 4000m CPU already requested. Only 200m available—not enough for a 2-CPU pod. The other workers show similar saturation.

Step 4: Identify the culprit.

$ kubectl top pods -n production --sort-by=cpu | head -5
NAME                          CPU    MEMORY
batch-processor-1             45m    512Mi
batch-processor-2             52m    489Mi
batch-processor-3             38m    501Mi
...

The batch processors request 500m CPU each (15 pods = 7.5 CPUs reserved) but actually use only ~50m. They are hoarding 7+ CPUs of headroom.

Step 5: Fix and verify.

The team reduces batch processor requests from 500m to 100m—still 2x actual usage for safety. After the rollout:

$ kubectl get pods -n production -l app=api-server
NAME                         READY   STATUS    RESTARTS   AGE
api-server-7f8b9c-x4k2m      1/1     Running   0          45s
api-server-7f8b9c-r8n3p      1/1     Running   0          42s
api-server-7f8b9c-j2m7q      1/1     Running   0          40s

Pods scheduled within seconds. The constraint was never complicated—six nodes truly lacked resources, and two were tainted. Once headroom existed, scheduling succeeded.

Total debugging time: Four commands, five minutes, one config change.


Building Your Diagnostic Reflex

When a pod is pending, resist the urge to delete and recreate it. The scheduler already told you what is wrong—you just need to read the message.

Start with kubectl describe pod. Parse the FailedScheduling message. Follow the decision tree. Run the diagnostic commands for that specific cause. Fix the constraint or change the cluster state. Verify the pod schedules.

Four commands, five minutes, one constraint at a time. Pending pods are not mysterious—they are the scheduler being explicit about what it needs. Learn to listen.