KubernetesXXVII · Scheduling FailuresScheduling and node lifecycle
Resource-driven failures — insufficient CPU, memory, and storage
What you'll learn
- Diagnose a resource-driven FailedScheduling event
- Distinguish the four scoring modes of NodeResourcesFit
- Compute the relationship between Pod requests and node capacity
- Apply the operational patterns for resource-driven failures
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
The most common scheduling failure is a Pod whose requests
exceed the node’s available capacity. The scheduler’s
NodeResourcesFit plugin (formerly a family of separate
plugins) handles the filter and the scoring. This lesson
walks the filter logic, the four scoring modes, and the
operational patterns that keep a cluster from running out
of capacity.
The filter logic
NodeResourcesFit runs in the filter stage. For each node,
the plugin computes the node’s available capacity by
subtracting the requests of every existing Pod from the
node’s allocatable capacity. The Pod under scheduling is
feasible on the node if its requests fit within the
available capacity.
flowchart LR
A[Pod under scheduling] --> B[NodeResourcesFit]
B --> C[Compute node's<br/>available CPU]
B --> D[Compute node's<br/>available memory]
B --> E[Compute node's<br/>available ephemeral storage]
C --> F{All fit?}
D --> F
E --> F
F -->|Yes| G[Node is feasible]
F -->|No| H[Reject with<br/>Insufficient resource]
The plugin does not consider limits. The filter is purely
about requests. The Pod’s spec.containers[].resources.limits
are used during runtime enforcement (cgroups) and during
eviction; the scheduler never looks at them.
The “available” computation is a sum across the node’s
existing Pods. The scheduler reads the node’s
Status.Allocatable, which is the node’s reported capacity
minus the kubelet’s reserved resources (the kubelet reserves
CPU and memory for the system and for the eviction
threshold).
# Substitute your own value before running:
NODE=worker-01
kubectl get node "$NODE" -o jsonpath='{.status.allocatable}' | jq
{
"cpu": "15800m",
"memory": "63154880Ki",
"ephemeral-storage": "47113644477",
"pods": "110"
}
# Substitute your own value before running:
NODE=worker-01
kubectl describe node "$NODE" | grep -A 5 "Allocated resources"
Allocated resources:
Resource Requests Limits
-------- -------- ------
cpu 9250m (58%) 16000m (101%)
memory 38Gi (62%) 60Gi (99%)
ephemeral-storage 0 (0%) 0 (0%)
The “Requests” column is what the scheduler sees. The “Limits” column is what the kubelet enforces. A node that shows 100% of its limits is not overcommitted; the limits are the runtime ceiling, and the requests are the scheduling ceiling.
The four scoring modes
The NodeResourcesFit plugin is also a scoring plugin. The
scoring mode is configurable by the cluster operator:
| Mode | Strategy | Best for |
|---|---|---|
LeastAllocated (default) | Score is highest for the node with the most free capacity | Bin-packing avoidance; balance across nodes |
MostAllocated | Score is highest for the node with the least free capacity | Bin-packing; maximum density |
BalancedAllocation | Score is highest for the node with the most balanced CPU/memory ratio | Mixed workloads; better caching locality |
VolumeBinding (different plugin) | Score is highest for the node that already has the volume attached | Stateful workloads |
The scoring mode is set via the scheduler’s profile
configuration. The default is LeastAllocated, which is the
right choice for most general-purpose clusters. The
MostAllocated mode is used by clusters that want to
aggressively bin-pack to reduce the node count.
Reading the failed-scheduling event
A resource-driven failure looks like:
0/5 nodes are available: 2 Insufficient memory, 3 Insufficient cpu.
The Insufficient memory and Insufficient cpu strings are
the format produced by the NodeResourcesFit filter. The
numbers are the count of nodes that failed for each reason.
A single node can fail for multiple reasons; the message
counts the dominant reasons.
To identify which node failed which resource, the operator must cross-reference the message with the node’s capacity:
kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory
NAME CPU MEMORY
node-1 16000m 63154880Ki
node-2 16000m 63154880Ki
node-3 8000m 31577440Ki
node-4 16000m 63154880Ki
node-5 16000m 63154880Ki
A Pod with requests.memory: 64Gi is feasible on
node-1, node-2, node-4, and node-5 (each has
64Gi+), but not on node-3 (which has only 32Gi). The
message “2 Insufficient memory” would mean the Pod has
more than 64Gi of memory request, and only the 32Gi node
and one other node fail.
Distinguishing requests from usage
The filter compares requests, not current usage. A node whose existing Pods are requesting 50Gi of memory but using only 20Gi is “50Gi allocated” from the scheduler’s perspective. The scheduler overcommits; the kubelet enforces.
This has two operational consequences:
- A Pod with a small request can land on a busy node. The filter does not care that the node is at 90% of its memory usage; it cares only that the node has the request-shaped capacity. The Pod is scheduled and runs. If the existing Pods grow, the new Pod is evicted.
- A Pod with a large request that actually uses little
is over-committed. A request of 16Gi for a workload
that uses 1Gi is a 16:1 overcommit. The cluster can
show plenty of capacity in
kubectl describe nodebut fail to schedule if the freed Pods were using 1Gi and the new Pod is requesting 16Gi.
The fix is to size the requests to the actual usage. A workload that uses 1Gi but requests 16Gi is reserving 15Gi of capacity that no other Pod can use. Production clusters should monitor request-to-usage ratio and right-size requests.
The four diagnostic moves
- Read the message.
Insufficient memoryandInsufficient cpuare the dominant failure modes;Insufficient ephemeral-storageis rare and usually indicates a workload writing too much to the container’s filesystem. - Identify the dominant node. The message lists the
count, not the node names. Use
kubectl describe nodeto find the node that has the resource. - Inspect the Pod’s requests. Many clusters fail
because a Pod’s
requestsare not set, and the scheduler assigns the default (theLimitRangedefault or the cluster’s default). Set the requests explicitly. - Inspect the node’s other Pods. A node that is failing to schedule new Pods but has plenty of capacity for existing Pods is a node that has a runaway workload or a workload that has not been reclaimed after a previous eviction.
The five fix patterns
- Scale the cluster. Add a node. The new node has fresh capacity; the scheduler can place the Pod there.
- Reduce the Pod’s requests. The Pod’s actual usage is often smaller than the request. Right-size the request based on the workload’s measured P95.
- Remove a Pod from the node. The Pod that is consuming the resource must be evicted or rescheduled elsewhere. The fix is rarely “delete the Pod”; it is “address the workload that is mis-using the node.”
- Set a
LimitRangewith a sane default. A namespace without aLimitRangeallows the Pod to be created with no requests, leading to an opaque “fits” or “doesn’t fit” decision. ALimitRangewith explicit defaults is more predictable. - Set a
ResourceQuotawith sufficient headroom. A namespace that is at quota is failing to schedule new Pods because the existing Pods have consumed the quota. The fix is to raise the quota or to free capacity.
Quiz
Knowledge check · 4 questions
Q1. A node shows 60% CPU utilisation, yet the scheduler reports `Insufficient cpu`. Why?
Q2. Reducing a Pod's CPU request lets it schedule onto a node that reported `Insufficient cpu`.
Q3. Explain a node that rejects a Pod for Insufficient memory while its dashboards show it half idle.
`analytics-runner` requests 8Gi of memory and is Pending with `0/6 nodes are available: 6 Insufficient memory`. The metrics backend shows every node between 28% and 41% memory usage. `kubectl describe node node-2` reports `Allocated resources: memory 58Gi (94%)` against an allocatable of 63154880Ki.
Q4. Which of `requests` and `limits` does NodeResourcesFit read, and why is a Node's `status.allocatable` smaller than its `status.capacity`?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Requests are the scheduler’s currency. The scheduler does not see limits. A Pod that uses 1Gi but requests 16Gi is reserving 16Gi of capacity that the cluster cannot use. Audit request-to-usage ratios at every release.
LeastAllocatedis the production default. The default scoring mode favours nodes with the most free capacity.MostAllocatedis appropriate only for clusters that want aggressive bin-packing and have a controller like the cluster-autoscaler to drain underused nodes.- A cluster that is permanently “Insufficient” is a cluster that needs capacity. The cluster-autoscaler is the production answer. The operator should set the autoscaler to react to the scheduler’s failure pattern.
- Ephemeral-storage is the silent capacity leak. A
workload that writes to
/tmpcan fill the node’s filesystem. TheInsufficient ephemeral-storagemessage is rare but the failure is real. Monitornode_stats_fs_*and setrequests.ephemeral-storageon Pods that write to the container filesystem. - Override the scheduler’s filter with a NodeResourceFit-score extension. A custom scoring plugin can prefer nodes with the right CPU/memory shape for the workload. The default scoring is a heuristic and may not match the workload’s actual preference.