Konrad Kowalski (rootsher)Principal Platform & Reliability Architect000111101000100100111000010000011010001100100001

CPU Requests in Kubernetes: what they really are

date
category
Containers
also in
Capacity & Performance
reading
4 min / 813 words

In the previous parts we established two things.

First, a process does not "own CPU" - the Linux scheduler hands it execution time.

Second, cgroups let you group processes and give those groups relative weights when accessing the processor.

Only now does requests.cpu start to make sense.

The most common wrong model looks like this:

text
requests.cpu: 500m

meaning:

text
the container gets half a CPU

No.

A CPU request is first of all a declaration of demand. Kubernetes uses that declaration to decide where a Pod may be started. Later the same information also influences the workload's relative CPU weight.

It is still not a limit.


What 500m means

CPU in Kubernetes is expressed in CPU units:

text
1 CPU    = 1000m
500m     = 0.5 CPU
250m     = 0.25 CPU

On a physical machine 1 CPU corresponds to one logical CPU as seen by the system. On a VM it will be a vCPU. Kubernetes treats that value as an absolute amount of the resource for accounting purposes.

Example:

yaml
resources:
  requests:
    cpu: 500m

therefore means:

text
I declare demand for 0.5 CPU

It does not mean:

text
cap me at 0.5 CPU

That distinction is crucial.


The request's first use: kube-scheduler

Assume a node:

text
Allocatable CPU: 8

Pods already scheduled on it:

text
Pod A: request 2 CPU
Pod B: request 3 CPU
Pod C: request 2 CPU

SUM = 7 CPU

Another one arrives:

yaml
resources:
  requests:
    cpu: "2"

For kube-scheduler the arithmetic looks like this:

text
7 + 2 = 9 CPU

The node only has:

text
8 CPU Allocatable

so the Pod cannot be scheduled there.

The most important detail: the scheduler does not have to look at current CPU usage.

The node may right now be using:

text
0.8 CPU

and still reject the Pod.

The scheduler compares requests, not momentary CPU usage. Kubernetes does this deliberately - a request is meant to represent capacity that must be accounted for in case the workload really needs it.

So we can say:

text
request = a reservation in kube-scheduler's model

But you have to be careful with the word "reservation".


The scheduler reserves capacity, the kernel does not set CPU time aside

Assume:

yaml
resources:
  requests:
    cpu: 500m

Once the container starts, the kernel does not create anything like this:

text
every second:

500 ms -> this container
500 ms -> the rest of the system

There is no statically reserved pool of 500m waiting only for this workload.

If the workload does nothing, the CPU can be used by somebody else.

And if the workload wants more CPU and the processor is free, it can use more than its request.

Example:

text
request = 500m
usage   = 1800m

is perfectly normal.

A request is not a ceiling.


The request's second use: relative share of CPU

Here we use the mechanism from the previous article.

Assume two workloads:

text
A request = 250m
B request = 750m

On the resource management side, request values are used to assign the corresponding CPU weights.

Historically, in cgroup v1, Kubernetes converted a request into cpu.shares:

text
cpu.shares = milliCPU * 1024 / 1000

so roughly:

text
250m -> 256 shares
750m -> 768 shares

which gives a proportion of:

text
1 : 3

In the cgroup v2 world the corresponding mechanism is cpu.weight; the exact conversion also depends today on the OCI runtime layer.

What matters most, though, is not the specific number but the mechanism:

text
larger request
        |
        v
larger relative CPU weight

What that gives in practice

We have one available CPU.

Both workloads are constantly runnable:

text
A request = 250m
B request = 750m

Because both want the processor all the time, we have contention.

Simplified, their shares may look like this:

text
A ~ 25%
B ~ 75%

Now B stops doing work.

Only A is left.

Can A still use at most 25% of the CPU?

No.

It can use practically:

text
A ~ 100%

The 250m request does not cap it at 25%.

That is precisely the consequence of the weight mechanism.

A request influences the split when competition exists.


Why a request is not "minimum CPU"

You often meet this simplification:

text
request = minimum
limit   = maximum

For CPU the word "minimum" is dangerous.

requests.cpu: 500m does not mean the process unconditionally receives at least:

text
500 ms CPU time

in every second.

A request influences resource accounting and priority during competition, but we are still talking about a shared scheduler.

It is much better to think:

text
request =
declared demand

rather than:

text
request =
a hard guaranteed slice of the processor

What if the request is too low

Assume a node:

text
16 CPU

We start 16 Pods.

Each declares:

text
request = 500m

The scheduler sees:

text
16 x 0.5 CPU = 8 CPU

So from its point of view the node still has plenty of free capacity.

But under production traffic every Pod wants to use:

text
2 CPU

The real demand is:

text
16 x 2 CPU = 32 CPU

on a node that has:

text
16 CPU

The scheduler did nothing wrong.

It received a declaration of:

text
500m

and made its placement decision on that basis.

It is the workload that consumes far more than we declared.

A request is therefore not just a technical setting.

It is an input to the capacity model of the whole cluster.


What if the request is too high

The opposite case is possible too.

The application really uses:

text
200m

but we declare:

text
request = 2 CPU

For the scheduler every replica then occupies:

text
2 CPU capacity

even if it almost never uses it.

The effect can be simple:

text
fewer Pods per node
more nodes
worse bin packing

So a request is a trade-off.

Too low, and the scheduler overestimates available capacity.

Too high, and we needlessly leave that capacity unused.


The most important distinction

A CPU request exists in two worlds at once.

Kubernetes

text
requests.cpu
    |
    v
resource accounting
    |
    v
the decision whether the Pod fits on a node

Linux

text
requests.cpu
    |
    v
CPU shares / weight
    |
    v
the workload's relative position during contention

That leads to a simple model:

text
requests.cpu: 500m

does not mean:

text
you have half a core

It rather means:

text
Kubernetes:
count me as a workload needing 0.5 CPU

Linux:
under competition, take into account the weight
derived from my declared request

And most importantly:

text
request != limit

A workload with a 500m request may consume far more than 500m if CPU is available.

A request describes declared demand and a relative share under competition.

It does not define the maximum amount of CPU a process may use.