Sign in
← Kubernetes: debugging
7 min · medium · Lesson 3 of 12

Pod Pending because of CPU

What FailedScheduling means, how requests differ from limits, and why a node with 4 cores won't give you 4 cores.

What FailedScheduling means, how requests differ from limits, and why a node with 4 cores won't give a Pod 4 cores.

Why this matters

A Pod is created but doesn't start. A minute, five minutes, an hour, the status doesn't change.

NAME    READY   STATUS    RESTARTS   AGE
heavy   0/1     Pending   0          2m

Pending means: the object exists in the API, but the container hasn't started yet. Most often the Pod is stuck before it even starts, at the node-selection stage. That's the job of the scheduler, a control plane component that picks a node for every new Pod. If there's no node that fits, the Pod will wait indefinitely, with no errors and no restarts. You need to be able to recognize this situation in a minute.

Why a Pod gets stuck in Pending

The Pending phase lasts from the Pod's creation until all its containers start. Several different causes fall into it, and the STATUS column in kubectl get pods tells them apart:

  1. The scheduler didn't find a node. Not enough resources, taints get in the way, affinity or nodeSelector didn't match, a PVC isn't bound. STATUS: Pending.
  2. The image is still downloading. A node is already chosen, and kubelet is pulling a large image. STATUS: ContainerCreating.
  3. An init container is running. The main containers are waiting for it to finish. STATUS: Init:0/1.

The first case is the most common, and it's also the least obvious at first glance: nothing is wrong with the Pod, there's just nowhere to put it. The rest of this lesson is about that case.

What the diagnostics show

kubectl describe pod heavy

The main section in the output is Events. When there's a problem choosing a node, you'll see a FailedScheduling event there:

Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  12s   default-scheduler  0/1 nodes are available: 1 Insufficient cpu. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.

Read the message left to right:

Common reasons:

TextWhat it means
Insufficient cpuThe new Pod's requests.cpu is more than what's free on the node
Insufficient memorySame thing, for memory
node(s) had untolerated taintThe nodes have a taint, and the Pod has no matching toleration
node(s) didn't match Pod's node affinity/selectornodeSelector or affinity didn't match
node(s) didn't have free ports for the requested pod portshostPort is already taken
pod has unbound immediate PersistentVolumeClaimsThe PVC isn't bound to a PersistentVolume

The exact same Insufficient cpu happens in two different situations. The Pod either waits for space to free up on the node, or it will never fit, because it asks for more than the biggest node has. You can only tell them apart by the node's numbers, more on that below.

Requests vs. limits

requests is how much the Pod is guaranteed to get. When picking a node, the scheduler looks only at requests. The sum of requests.cpu of all Pods on a node can't exceed allocatable (more on that below).

limits sets a ceiling the container can't rise above while running. On their own, limits don't affect node selection.

There's a catch. If a container has a limit set but no request, Kubernetes copies the limit into the request. A manifest with just limits.cpu: 8 is read by the scheduler as a request for 8 cores. You can check what actually happened on the created Pod:

kubectl get pod heavy -o jsonpath='{.spec.containers[0].resources}'
{"limits":{"cpu":"8"},"requests":{"cpu":"8"}}

If a Pod requests requests.cpu: 64, the scheduler looks for a node with at least 64 CPU free. In a cluster of 4-core nodes, no such node exists, and the Pod stays in Pending forever.

A requests value bigger than the biggest node's allocatable will never be scheduled. There's no error on creation: the Pod just sits in Pending until it's deleted.

CPU and memory units

CPU:

m stands for millicpu, a thousandth of a CPU. It has nothing to do with megahertz.

Memory:

Without a suffix, CPU is whole cores and memory is bytes. So cpu: 64 reads as "give me 64 cores", while cpu: 64m reads as sixty-four thousandths of a core. One letter changes the request a thousand times over.

Capacity vs. allocatable

kubectl describe node shows two different sets of numbers:

Capacity:
  cpu:                4
  memory:             8Gi
Allocatable:
  cpu:                3800m
  memory:             7Gi

Capacity shows how much the node physically has. Allocatable is usually smaller: the administrator reserves part of the resources for kubelet, the container runtime, and system daemons. The scheduler counts by allocatable. On local clusters the reserve is often not configured, and the numbers match.

Further down in the same output, you can see what's already taken:

Allocated resources:
  Resource           Requests     Limits
  --------           --------     ------
  cpu                500m (13%)   1 (26%)
  memory             256Mi (3%)   512Mi (7%)

These are the sums of requests and limits of all Pods on the node, not the real load. The free space the scheduler sees is calculated as Allocatable - Requests from Allocated resources. If a new Pod asks for more, the scheduler replies Insufficient cpu.

allocatable  3800m  ████████████████████
in use        500m  ███
free         3300m  █████████████████
Pod asks    64000m  doesn't fit

Two conclusions follow. A Pod that asks for more than allocatable won't fit even on an empty node. A Pod that asks for less than allocatable, but more than what's free, will run as soon as space opens up on the node.

How to fix it

There are three ways.

First: lower the new Pod's requests to its real consumption. Rough but workable guides: 100m for a light service, 500m for a medium one, 2 for a heavy one. Exact numbers come from metrics, for example kubectl top pod on a running copy (needs metrics-server).

Second: free up space. Often a node isn't taken up by real load, but by neighbors' inflated requests. Allocated resources and the list of Pods on the node show who reserved how much.

Third: add a node. In the cloud this is done by the cluster autoscaler, which reacts to exactly this kind of Pending Pod.

If the Pod was created by a Deployment, edit the Deployment's template, and the Pods recreate themselves:

kubectl set resources deployment web --requests=cpu=200m

A standalone Pod is easier to delete and recreate with a fixed manifest. A plain kubectl apply with new resources on an existing Pod is rejected by the API. Starting with Kubernetes 1.35, a running Pod's CPU and memory can be changed in place through the resize subresource (kubectl patch pod ... --subresource resize), but that's a separate topic.

Useful commands when investigating:

# The reason is at the end of describe
kubectl describe pod heavy | tail -10

# How much the node has and how much is taken
kubectl describe node | grep -A5 "Allocatable:"
kubectl describe node | grep -A5 "Allocated resources"

# How much the Pod requests
kubectl get pod heavy -o jsonpath='{.spec.containers[0].resources}'

Common mistakes

To catch these mistakes earlier, namespaces get a LimitRange with default and maximum values. Then a Pod without requests gets reasonable numbers, and a Pod with cpu: 64 is rejected by the API at creation, with a clear error instead of a silent Pending.

In short

What you'll need for the tasks

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson k8s-debug-03-pod-pending-cpu