Konrad Kowalski (rootsher)Principal Platform & Reliability Architect101010100111001001001100101000101111011110101001

Requests, Limits and QoS: how Kubernetes classifies Pods

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

We already have three pieces of the puzzle:

text
requests.cpu -> declared demand + relative weight
limits.cpu   -> maximum CPU bandwidth
contention   -> competition for the available CPU

Kubernetes adds one more layer on top:

text
QoS class

Every Pod gets one of three classes:

text
Guaranteed
Burstable
BestEffort

The names sound as if they directly determined "CPU quality", but that is far too big a simplification.

A QoS class is above all a classification derived from how CPU and memory requests/limits are configured. Kubernetes uses it, among other things, when making decisions under resource pressure.

In this text we care only about how the classes come about and what they mean in a CPU context.


BestEffort

The simplest case:

yaml
containers:
- name: app
  image: example/app

No:

text
requests.cpu
limits.cpu
requests.memory
limits.memory

The Pod gets the class:

text
BestEffort

For a Pod to be BestEffort, none of its containers may have a CPU or memory request or limit.

That does not mean:

text
the Pod cannot use CPU

Quite the opposite.

If the node has spare compute power, BestEffort can use it.

For example:

text
node = 16 CPU

other workloads use = 4 CPU

BestEffort workload wants = 8 CPU

it may really get those 8 CPU if nothing else gets in its way.

So BestEffort does not mean:

text
little CPU

It rather means:

text
no declared CPU request
no CPU limit

Burstable

Now let us set:

yaml
resources:
  requests:
    cpu: 500m

There is no limit.

The Pod does not meet the criteria for Guaranteed, but it does have a resource configuration.

So it lands in:

text
Burstable

A Pod goes into the Burstable class when it:

text
is not Guaranteed

and has at least one CPU or memory request or limit.

Example:

yaml
resources:
  requests:
    cpu: 500m
  limits:
    cpu: "2"

will also be a typical Burstable case, assuming the rest of the Pod's configuration does not meet the conditions for Guaranteed.

In a CPU context that means we can have:

text
request = 500m
limit   = 2 CPU

that is:

text
scheduler accounting = 0.5 CPU

relative CPU weight = derived from 500m

maximum bandwidth = 2 CPU

QoS does not replace requests and limits.

It is a classification built on top of them.


Guaranteed

Now this configuration:

yaml
resources:
  requests:
    cpu: "1"
    memory: 1Gi
  limits:
    cpu: "1"
    memory: 1Gi

If every container in the Pod has positive CPU and memory requests and limits, and for each resource:

text
request == limit

the Pod gets the class:

text
Guaranteed

So for CPU:

text
requests.cpu = limits.cpu

but that alone is not enough.

Memory has to meet the matching conditions too.

That is an important detail.

This configuration:

yaml
resources:
  requests:
    cpu: "1"
  limits:
    cpu: "1"

does not automatically mean:

text
Guaranteed

if we did not also satisfy the memory requirements.

QoS is a classification of the whole Pod, not of CPU alone.


Guaranteed does not mean dedicated CPU

The name may suggest:

text
Guaranteed
=
I have a guaranteed physical core

No.

A Pod with:

yaml
resources:
  requests:
    cpu: "1"
    memory: 1Gi
  limits:
    cpu: "1"
    memory: 1Gi

may be Guaranteed and still run on shared CPUs.

From the point of view of the mechanisms we already know:

text
request = 1 CPU
limit   = 1 CPU

means:

text
the scheduler accounts for 1 CPU of capacity

the workload has a CPU weight derived from the request

the workload has a bandwidth limit matching 1 CPU

It does not mean:

text
CPU #7 belongs to this Pod alone

Exclusive CPUs require the additional CPU Manager mechanism and matching node configuration. Kubernetes documents that Guaranteed Pods may qualify for exclusive CPUs under the static policy, but the Guaranteed class alone does not create such an assignment.


Three Pods on one CPU

Assume one CPU and three workloads.

Pod A - BestEffort

yaml
resources: {}

Pod B - Burstable

yaml
resources:
  requests:
    cpu: 500m

Pod C - Guaranteed

yaml
resources:
  requests:
    cpu: "1"
    memory: 1Gi
  limits:
    cpu: "1"
    memory: 1Gi

You should not think:

text
Guaranteed always first
Burstable next
BestEffort last

The Linux scheduler does not interpret Kubernetes QoS names that way.

The runtime works with cgroup mechanisms.

Pod B has a CPU weight derived from its request:

text
500m

Pod C has a weight derived from:

text
1 CPU

and additionally has a CPU bandwidth limit of:

text
1 CPU

Pod A has no declared CPU request, so its position under contention will be correspondingly weaker, according to the cgroup configuration the kubelet generated.

This is not classic scheduler priority:

text
Guaranteed > Burstable > BestEffort

A QoS class on its own is not an equivalent of nice, of scheduler priority, or of a CPU priority queue.


Most importantly: QoS is a result of configuration

A good mental model:

text
requests + limits
       |
       v
Kubernetes computes the QoS class
       |
       v
Guaranteed / Burstable / BestEffort

Not the other way round.

We do not configure directly:

yaml
qosClass: Guaranteed

Kubernetes computes the class from the resource configuration.

You can see it for example with:

bash
kubectl get pod <pod> -o jsonpath='{.status.qosClass}'

and get:

text
Guaranteed

or:

text
Burstable

or:

text
BestEffort

The class is assigned when the Pod is created and stays its class for the Pod's lifetime; current in-place resize mechanisms do not let a resource change move it to a different QoS class.


One trap: a limit with no explicit request

Assume:

yaml
resources:
  limits:
    cpu: "1"

with no:

yaml
requests:
  cpu:

You should not automatically assume:

text
CPU request = 0

If a request was not given, Kubernetes may set the request to the value of the limit. As a result the container can end up with:

text
request = 1 CPU
limit   = 1 CPU

That matters both for the scheduler and for determining QoS.

So when analysing a running Pod it is worth looking at the final resource configuration, not only at the fragment of the manifest you remember from the repository.


QoS does not change what a request or a limit means

This matters, so that we do not create another abstraction where there is none.

For Burstable:

yaml
requests:
  cpu: 500m
limits:
  cpu: "2"

still:

text
500m

is the request, and:

text
2 CPU

is the bandwidth limit.

For Guaranteed:

yaml
requests:
  cpu: "1"
limits:
  cpu: "1"

these are still the same two mechanisms.

Their values simply happen to be equal.

QoS does not create a new way of scheduling CPU.


Mental model

Put simply:

text
BestEffort

request = none
limit   = none
text
Burstable

any CPU/memory configuration
that does not satisfy Guaranteed
text
Guaranteed

for every container:
CPU request = CPU limit > 0
Memory request = Memory limit > 0

In a CPU context, though, what matters most is:

text
QoS class
!=
CPU priority class

and:

text
Guaranteed
!=
dedicated CPU

A request still governs declared capacity and relative weight.

A limit still governs maximum CPU bandwidth.

QoS is a Kubernetes classification layer built on those settings, not a third, independent mechanism for handing out CPU.