Fix: Kubernetes Pod stuck in Pending state
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix Kubernetes Pod stuck in Pending state caused by insufficient resources, unschedulable nodes, PVC issues, node selectors, taints, and resource quotas.
The Error
You deploy a Pod and it stays in Pending status indefinitely:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-app-7b4f5c8d9-xk2lm 0/1 Pending 0 10mRunning kubectl describe pod shows events like:
Events:
Warning FailedScheduling default-scheduler 0/3 nodes are available:
1 node(s) had untolerated taint {node.kubernetes.io/not-ready: },
2 node(s) didn't match Pod's node affinity/selector,
3 Insufficient cpu.Or:
Warning FailedScheduling 0/5 nodes are available: 5 Insufficient memory.Warning FailedScheduling pod has unbound immediate PersistentVolumeClaims.The Kubernetes scheduler cannot find a node to place the Pod on. The Pod sits in Pending until the scheduling constraint is resolved.
Why This Happens
A Pod enters Pending when the scheduler cannot find a suitable node. The scheduler runs a two-phase loop: a filtering phase that eliminates nodes that cannot run the Pod, and a scoring phase that ranks the survivors. If filtering removes every node, the Pod never reaches scoring and stays Pending until the constraint changes.
Filtering checks the following predicates, in this rough order:
- Resource requests. Does any node have enough CPU and memory in its allocatable budget?
- Node selectors and affinity. Does the Pod require specific node labels that match a real node?
- Taints and tolerations. Is the node tainted, and does the Pod tolerate every taint with a matching effect?
- PersistentVolumeClaims. Are the requested volumes bound, and is the volume’s zone reachable from a candidate node?
- Resource quotas. Has the namespace exceeded its CPU, memory, or Pod count quota?
- Pod topology constraints. Are there spread constraints that cannot be satisfied with the current node pool?
The kubectl describe pod events tell you exactly which predicate failed and on how many nodes. The format 0/N nodes are available: X reasons lists every distinct rejection reason and the count of nodes that hit it. Always read this block first — the rest of this article is unblocking those specific reasons.
One more subtlety. The scheduler does not consider current node utilization, only the sum of resource requests from already-scheduled Pods. A node sitting at 5% CPU usage but with requests totaling 95% will still reject your Pod. This is why oversized requests are the single most common cause of phantom Pending Pods.
In Production: Incident Lens
In production, Pending Pods are a capacity event. Existing Pods continue to serve traffic, so user-facing SLOs usually hold for the first few minutes, but anything that needs a new Pod stalls — rolling deploys, HPA scale-out, node replacements, and Job runs all freeze. The blast radius is “no new Pods of any kind can start” until the constraint clears.
The standard monitoring signal is kube_pod_status_phase{phase="Pending"} exceeding a threshold for more than five minutes, ideally split by namespace so a stuck Job in one team’s namespace does not page the whole org. Pair it with cluster_autoscaler_unschedulable_pods_count — if both are non-zero and growing, the autoscaler tried to add a node and failed (quota, AMI, instance type unavailable in the zone). The recovery sequence is: confirm describe pod shows a real resource shortage rather than a transient unschedulable flag, manually scale the node group if the autoscaler is wedged, and cordon any node that is reporting pressure to stop the scheduler from retrying against it.
Postmortem preventives are almost always the same: enforce resource request budgets per namespace via LimitRange, run a descheduler to rebalance long-running Pods that drifted into hot nodes, and set maxUnavailable and maxSurge on rolling deploys so the deploy does not deadlock when capacity is tight. Tracking median request vs actual usage per workload is the highest-signal metric for catching oversized requests before they fill the cluster.
Fix 1: Fix Insufficient CPU or Memory
The most common cause. No node has enough free resources:
# Check the events
kubectl describe pod my-app-7b4f5c8d9-xk2lm | grep -A 10 Events
# Check node resource usage
kubectl top nodes
kubectl describe nodes | grep -A 5 "Allocated resources"Reduce resource requests:
# Before — requesting too much
resources:
requests:
cpu: "4"
memory: "8Gi"
# After — right-sized
resources:
requests:
cpu: "500m" # 0.5 CPU cores
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"Check what is consuming resources on the nodes:
# List all pods sorted by CPU request
kubectl get pods --all-namespaces -o custom-columns=\
"NAMESPACE:.metadata.namespace,NAME:.metadata.name,CPU:.spec.containers[*].resources.requests.cpu,MEM:.spec.containers[*].resources.requests.memory" \
| sort -k3 -hScale the cluster (add more nodes):
# EKS
eksctl scale nodegroup --cluster my-cluster --nodes 5 --name my-nodegroup
# GKE
gcloud container clusters resize my-cluster --num-nodes 5
# AKS
az aks scale --resource-group myRG --name myCluster --node-count 5Pro Tip: Set resource
requeststo what the Pod actually needs under normal load, andlimitsto the maximum it should ever use. Overly generous requests waste cluster capacity. Usekubectl top podsto measure actual usage before setting requests.
Fix 2: Fix Node Selector and Affinity Issues
The Pod requires specific node labels that no node has:
# Pod requires nodes labeled with gpu=true
spec:
nodeSelector:
gpu: "true"Check available node labels:
kubectl get nodes --show-labels
# or
kubectl get nodes -L gpuFix: Add the label to a node:
kubectl label nodes my-node gpu=trueFix: Remove or update the node selector:
spec:
# Remove nodeSelector if not needed
# Or use nodeAffinity for soft preferences
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: gpu
operator: In
values: ["true"]preferredDuringScheduling is a soft constraint — the scheduler prefers matching nodes but will use others if none match. requiredDuringScheduling is hard — the Pod stays Pending until a matching node exists.
Fix 3: Fix Taints and Tolerations
Nodes might have taints that prevent scheduling:
# Check node taints
kubectl describe node my-node | grep Taints
# Taints: node.kubernetes.io/not-ready:NoScheduleCommon taints:
| Taint | Meaning |
|---|---|
node.kubernetes.io/not-ready | Node is not healthy |
node.kubernetes.io/unreachable | Node is unreachable |
node.kubernetes.io/disk-pressure | Node disk is full |
node.kubernetes.io/memory-pressure | Node is low on memory |
node-role.kubernetes.io/control-plane | Control plane node |
Add tolerations to the Pod:
spec:
tolerations:
- key: "gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"Remove a taint from a node:
kubectl taint nodes my-node gpu=true:NoSchedule-
# The trailing - removes the taintFor control plane nodes (single-node clusters):
kubectl taint nodes --all node-role.kubernetes.io/control-plane-Common Mistake: Running a single-node cluster (like Minikube or kind) and wondering why Pods are Pending. Control plane nodes are tainted by default. Either add tolerations or remove the taint.
Fix 4: Fix PersistentVolumeClaim Issues
If the Pod uses a PVC that is not bound:
Warning FailedScheduling pod has unbound immediate PersistentVolumeClaimsCheck PVC status:
kubectl get pvc
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
# my-data Pending standardCommon causes:
# No PersistentVolume matches the PVC
kubectl get pv
# StorageClass doesn't exist
kubectl get storageclass
# The storage provisioner is not running
kubectl get pods -n kube-system | grep provisionerFix: Create a PersistentVolume:
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /data/my-pvFix: Use a valid StorageClass:
# List available storage classes
kubectl get storageclass
# Update PVC to use an existing storage classapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard # Must match an existing StorageClass
resources:
requests:
storage: 10GiFor cloud providers, check the volume zone:
# The PVC might request a zone where no nodes exist
kubectl describe pvc my-dataVolumes are often zone-specific. If your node is in us-east-1a but the volume is in us-east-1b, the Pod cannot be scheduled.
Fix 5: Fix ResourceQuota Limits
The namespace might have resource quotas that prevent scheduling:
kubectl describe resourcequota -n my-namespace
# Name: my-quota
# Resource Used Hard
# -------- ---- ----
# cpu 3800m 4000m
# memory 7Gi 8Gi
# pods 19 20Fix: Increase the quota:
apiVersion: v1
kind: ResourceQuota
metadata:
name: my-quota
namespace: my-namespace
spec:
hard:
cpu: "8"
memory: "16Gi"
pods: "40"Fix: Reduce resource requests on existing Pods:
# Find Pods using the most resources
kubectl top pods -n my-namespace --sort-by=cpuFix: Clean up unused Pods:
# Delete completed jobs
kubectl delete jobs --field-selector status.successful=1 -n my-namespace
# Delete evicted pods
kubectl delete pods --field-selector status.phase=Failed -n my-namespaceFix 6: Fix LimitRange Issues
A LimitRange might set minimum resource requirements higher than your Pod specifies:
kubectl describe limitrange -n my-namespace# If LimitRange requires minimum 256Mi memory but your Pod requests 128Mi,
# the Pod will not be admitted
Type Resource Min Max Default Request Default Limit
---- -------- --- --- --------------- -------------
Container cpu 100m 2 200m 500m
Container memory 256Mi 4Gi 256Mi 512MiFix: Increase your Pod’s resource requests to meet the minimum:
resources:
requests:
cpu: "100m"
memory: "256Mi" # Must meet the LimitRange minimumFix 7: Fix Pod Topology Spread Constraints
Spread constraints can prevent scheduling if there are not enough nodes:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule # This makes it a hard constraint
labelSelector:
matchLabels:
app: my-appIf you have 3 nodes and already have 1 Pod on each, a 4th Pod cannot satisfy maxSkew: 1 with DoNotSchedule.
Fix: Use ScheduleAnyway:
whenUnsatisfiable: ScheduleAnyway # Soft constraint — best effortFix: Add more nodes or reduce replicas.
Fix 8: Debug Scheduling with Events and Logs
Check Pod events:
kubectl describe pod <pod-name> | tail -20Check scheduler logs:
kubectl logs -n kube-system -l component=kube-scheduler --tail=50Simulate scheduling:
# Dry-run to see where a Pod would be placed
kubectl get nodes -o json | jq '.items[] | {name: .metadata.name, allocatable: .status.allocatable}'Force-reschedule all Pending Pods:
# Delete and let the Deployment recreate them
kubectl delete pod <pod-name>Still Not Working?
Check for node cordons. A node might be cordoned (marked as unschedulable):
kubectl get nodes
# NAME STATUS ROLES AGE
# node-1 Ready,SchedulingDisabled <none> 30d
# Uncordon the node
kubectl uncordon node-1Check for PodDisruptionBudgets (PDBs). A PDB might prevent rescheduling during rolling updates.
Check for init containers. If an init container cannot complete (e.g., waiting for a service), the Pod stays in a Pending-like state (Init:0/1).
Check the cluster autoscaler decision log. If you run an autoscaler, it logs why it did or did not add a node. The most common silent failure is hitting a cloud quota (EC2 vCPU limit, GCE in-use IP addresses, AKS subnet exhaustion). The Pod stays Pending forever because the autoscaler asks for capacity, gets rejected, backs off, and never retries. Check the autoscaler Pod logs or the cloud console quota page:
kubectl logs -n kube-system -l app=cluster-autoscaler --tail=200 | grep -iE "quota|exceed|not avail"Check for instance type availability in the zone. Spot or specific GPU instance types are not always available in every zone. The autoscaler may try, fail, and silently skip. Either expand the node group to multiple zones or fall back to a more common instance family.
Check for kubelet not registering the node. A node that has booted but whose kubelet cannot reach the API server (bad token, expired certificate, security group blocking 10250) will not appear in kubectl get nodes. Capacity exists on the VM but Kubernetes cannot use it. Restart kubelet and check /var/log/kubelet.log for handshake failures.
Check for stuck Pending evictions on a parent node. When a node enters NotReady, the controller manager tries to evict its Pods after the toleration window. If those evicted Pods have anti-affinity rules that prevent them from landing elsewhere, they stay Pending while the original node sits cordoned indefinitely.
For Pods that start but immediately crash, see Fix: Kubernetes Pod CrashLoopBackOff. For image pull failures, see Fix: Kubernetes ImagePullBackOff. For OOM kills, see Fix: Kubernetes Pod OOMKilled. For namespace quota errors that surface as admission rejections rather than Pending Pods, see Fix: Kubernetes resource quota exceeded.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Helm Not Working — Release Already Exists, Stuck Upgrade, and Values Not Applied
How to fix Helm 3 errors — release already exists, another operation is in progress, --set values not applied, nil pointer template errors, kubeVersion mismatch, hook failures, and ConfigMap changes not restarting pods.
Fix: Kubernetes HPA Not Scaling — HorizontalPodAutoscaler Shows Unknown or Doesn't Scale
How to fix Kubernetes HorizontalPodAutoscaler issues — metrics-server not installed, CPU requests not set, unknown metrics, scale-down delay, custom metrics, and KEDA.
Fix: Kubernetes Secret Not Mounted — Pod Cannot Access Secret Values
How to fix Kubernetes Secrets not being mounted — namespace mismatches, RBAC permissions, volume mount configuration, environment variable injection, and secret decoding issues.
Fix: Kubernetes Pod OOMKilled — Out of Memory Error
How to fix Kubernetes OOMKilled errors — understanding memory limits, finding memory leaks, setting correct resource requests and limits, and using Vertical Pod Autoscaler.