CoursesKubernetes administrationRequests, limits & QoS

Requests, limits & QoS

How requests drive placement and eviction order.

Intermediate12 min · lesson 27 of 65
In plain terms
Requests are the seat you reserve; limits are how loud you’re allowed to get before you’re asked to leave. Reserve nothing and you’re the very first to be bumped when the place fills up.

A container dies at 3 a.m. with exit code 137, and the pager blames a memory leak. Usually there's no leak. There's a missing number in the YAML. Two settings, the memory request and the memory limit, decide whether your Pod (the smallest deployable unit in Kubernetes, one or more containers that share a network address) gets a fair slice of a machine or gets shoved off it. A lot of 'the cluster feels unstable' tickets start right here.

Requests reserve, limits cap

Start with a dinner reservation. You call ahead, the restaurant holds a table for your party, and even on a packed Friday that seat stays yours whether you show up early or late. A request works the same way. It's the amount of CPU and memory the scheduler (the control-plane component that decides which node each Pod lands on) sets aside for your container before it ever runs. That capacity is reserved even while the container sits idle. A limit is different. It's the fire-code ceiling the kubelet (the agent Kubernetes runs on every node) enforces once the container is alive and using resources.

CPU and memory hit that ceiling very differently, and this is where people get burned. CPU is squeezable. Push past your CPU limit and the Linux kernel just hands you fewer time slices, so the app runs slower but stays up. That slowdown is called throttling. Memory can't be squeezed. Push past your memory limit and there's nowhere to put the extra bytes, so the kernel kills the process outright. That's an OOMKill (out of memory), and exit code 137 is its fingerprint (128 plus signal 9, the kill signal).

web-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: nginx:1.27
resources:
requests:
cpu: "250m" # scheduler reserves a quarter core
memory: "256Mi" # scheduler reserves 256Mi
limits:
memory: "256Mi" # kubelet OOMKills the container past this
output
$ kubectl apply -f web-pod.yaml
pod/web created
$ kubectl get pod web -o jsonpath='{.status.qosClass}{"\n"}'
Burstable

That cpu value, 250m, reads as 250 millicores, a quarter of one CPU core (1000m is a full core). That request pulls double duty. Besides telling the scheduler how much to set aside, it also fixes your container's share of CPU time when the node is busy, so a container asking for 250m gets half the CPU of one asking for 500m when they're both straining against the same cores. Notice there's no CPU limit here on purpose, only a memory limit. Hold that thought. And notice what came back: Burstable. You didn't ask for a class. Kubernetes worked it out from the numbers you set.

The QoS class you never set

You never write a Quality of Service (QoS) class anywhere in your manifest. Kubernetes reads it off your requests and limits the way an emergency room assigns triage priority from your vital signs, not from what you claim at the front desk. Three classes exist, and they set the order Pods get sacrificed when a node runs short on memory. Guaranteed means every container has its request equal to its limit for both CPU and memory; these go last. BestEffort means you set nothing at all, no requests and no limits; these go first. Everything in between is Burstable. Your web Pod landed in Burstable because it requests CPU but never limits it, so request and limit don't match across the board.

check the class on every pod
$ kubectl get pods -o custom-columns=NAME:.metadata.name,QOS:.status.qosClass
output
NAME QOS
cache Guaranteed
web Burstable
scratch BestEffort

Here's what actually happens under pressure. When a node's free memory falls past the kubelet's eviction threshold, the kubelet doesn't sit and wait for the kernel to start killing things at random. It ranks the Pods and evicts on purpose: BestEffort first, then Burstable Pods that have climbed above their requests, and Guaranteed last. At the same moment the kubelet writes each container's oom_score_adj value into the kernel, so if the kernel's own out-of-memory killer fires first during a sudden spike, it picks the same low-priority victims. The class shows up in both places, the orderly kubelet eviction and the kernel's panic kill.

How Kubernetes derives your QoS class
Kubernetes reads requests & limits on every container
the QoS class is computed, never written by you
nothing set anywhere
BestEffort
no requests, no limits, evicted FIRST under memory pressure
requests == limits, cpu AND memory, all containers
Guaranteed
evicted LAST, killed only as a final resort
anything in between
Burstable
evicted after BestEffort, before Guaranteed
You set the numbers; Kubernetes assigns the class. Set nothing and you're BestEffort, first over the side when a node runs out of memory.

Stop BestEffort sneaking into production

Trusting every developer to remember these fields is exactly how BestEffort Pods slip into a namespace. A LimitRange fixes it at the source. Think of it as the default portion size the kitchen plates up when a customer forgets to order one: it stamps default requests and limits onto any container that omits them, so a forgotten resources block becomes a sane Burstable Pod instead of a BestEffort one that dies first. Apply it once per namespace and every future Pod inherits the floor.

limitrange.yaml
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: payments
spec:
limits:
- type: Container
default: # applied as the LIMIT when you omit one
cpu: "500m"
memory: "256Mi"
defaultRequest: # applied as the REQUEST when you omit one
cpu: "100m"
memory: "128Mi"
output
$ kubectl apply -f limitrange.yaml
limitrange/default-limits created
$ kubectl run tmp --image=nginx --restart=Never -n payments
pod/tmp created
$ kubectl get pod tmp -n payments -o jsonpath='{.status.qosClass}{"\n"}'
Burstable

The tmp Pod set no resources at all, yet it came back Burstable, not BestEffort. The LimitRange backfilled the request and the limit before the Pod was admitted, so it schedules accurately and resists eviction instead of being first in line.

Reading the wreckage when it breaks

When a container keeps restarting, its status tells you why before any log line does. kubectl describe records the last time the process died and the reason. An OOMKill leaves an unmistakable trail: reason OOMKilled, exit code 137, and a restart count that keeps climbing every couple of minutes.

why did it restart
$ kubectl describe pod web | grep -A6 'Last State'
output
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Thu, 16 Jul 2026 02:58:41 +0000
Finished: Thu, 16 Jul 2026 03:01:12 +0000
Restart Count: 4

OOMKilled with a rising restart count means the real memory the app needs is above the limit you gave it. The fix is to measure actual usage (kubectl top pod, or your metrics) and raise the memory request and limit to match, not to delete the limit and let one leaky container eat the whole node. Raising the request to what the app really uses pays off at eviction time too. The kubelet only targets Pods running above their requests, so a request that matches reality keeps yours off the early kill list.

A CPU limit can throttle an app that has plenty of headroom
The most common self-inflicted latency bug is a CPU limit set too tight. The kernel enforces CPU limits in short windows of about 100 milliseconds each. A limit of 500m means your container gets roughly 50ms of CPU per window, and once it spends that, it waits for the next window even if the node has idle cores sitting right there. A bursty, latency-sensitive service can stall for tens of milliseconds this way while the node's overall CPU reads 30 percent. The usual fix: set CPU requests so scheduling and fairness still work, leave CPU off the limits, and always keep a memory limit to protect the node. Confirm the diagnosis with the container_cpu_cfs_throttled_seconds_total metric before you blame the code.

CPU limits throttle; memory limits OOMKill. Those failure modes feel different to users.

Right-sizing starts from observed usage, not from aspirational spreadsheets. kubectl top is a beginning, not a capacity plan.

LimitRanges can default requests in a namespace so bare pods stop landing as BestEffort by accident.

Try this

Run three pods: requests only, requests+limits equal, and limits without requests. Check QoS class in each pod status and discuss eviction order.

terminal
$ kubectl apply -f web-pod.yaml
pod/web created
$ kubectl get pod web -o jsonpath='{.status.qosClass}{"\n"}'
Burstable
$ kubectl get pods -o custom-columns=NAME:.metadata.name,QOS:.status.qosClass
$ kubectl apply -f limitrange.yaml
limitrange/default-limits created
$ kubectl run tmp --image=nginx --restart=Never -n payments
pod/tmp created
$ kubectl get pod tmp -n payments -o jsonpath='{.status.qosClass}{"\n"}'
Burstable
$ kubectl describe pod web | grep -A6 'Last State'

Takeaway

Requests drive scheduling and fair share; limits cap usage. QoS Guaranteed, Burstable, and BestEffort decide eviction pain under node pressure.

Quick check
01A latency-sensitive API has requests.cpu 200m and limits.cpu 500m. It runs fine most of the time, but every few seconds one request takes 300ms instead of 5ms. The node's CPU sits around 30 percent. Most likely cause?
Correct — CFS quota enforcement is per-window, so a bursty app can be throttled while the node looks half-idle. Confirm with container_cpu_cfs_throttled_seconds_total.
Incorrect — An OOMKill terminates the container with exit 137 and shows in the restart count; it would not produce a 300ms blip while the process keeps serving.
Incorrect — Node CPU is only 30 percent, so there's no contention to fight over. The cap here is self-imposed by the pod's own CPU limit, not the neighbors.
Incorrect — QoS class governs memory eviction order, not CPU scheduling, and this pod sets requests and limits anyway, so it isn't BestEffort.
02A container sets memory request and limit both to 256Mi, a CPU request of 250m, and no CPU limit. Which QoS class does Kubernetes assign, and why?
Correct — a single missing limit (here CPU) drops it out of Guaranteed into Burstable.
Incorrect — No: Guaranteed needs matching request and limit for CPU as well, and the missing CPU limit disqualifies it.
Incorrect — No: BestEffort means no requests or limits at all, and this pod sets several, so it cannot be BestEffort.
Incorrect — No: a CPU request is not a CPU limit; Guaranteed needs request equal to limit on both, not merely a request present.
03kubectl describe pod shows Last State Terminated, Reason OOMKilled, Exit Code 137, and a Restart Count climbing every couple of minutes. What is happening and the right fix?
Incorrect — No: throttling slows an app but never kills it; OOMKilled and exit 137 point at memory, not CPU.
Incorrect — No: node-pressure eviction reads as Evicted, not an OOMKilled container hitting its own limit; this is per-container.
Incorrect — No: that lets one leaky container consume the whole node, which the lesson warns against.
Correct — exit 137 (128+9) is the OOM fingerprint; size the request and limit to measured usage rather than removing the limit.

Related