Requests and limits — CPU and memory are not analogous
What happens when a container exceeds its CPU limit versus its memory limit, why the two answers differ, and what that means for how you set them.
Requests and limits look like one concept applied to two resources. They aren’t, and the difference is the source of a lot of confusion about why pods die.
Requests and limits do different jobs
Requests are for the scheduler. A request is a reservation: the scheduler will only place a pod on a node with that much unallocated capacity. Requests don’t constrain what a container uses at runtime.
Limits are for the kernel on the node. A limit is a ceiling enforced while the container runs. The scheduler doesn’t consider limits when placing pods.
Three consequences follow. A pod with a high request and no limit can consume an entire node. A pod with a low request and a high limit will schedule easily and then fight its neighbours. And a node can be scheduled to 100% of requests while sitting nearly idle, because requests are a claim rather than a measurement.
Exceeding a CPU limit: throttling
CPU is a compressible resource. Exceeding its limit doesn’t kill the container; the kernel throttles it. The process keeps running, just more slowly, in enforced idle periods.
This is where the trouble hides, because throttling is nearly invisible. No event, no restart, no obvious log line. Your service just gets slower, and the latency graph shows p99 spikes that correlate with nothing in the application.
The signal to watch is container_cpu_cfs_throttled_periods_total against container_cpu_cfs_periods_total. If a meaningful fraction of periods are throttled, your limit is the bottleneck rather than your code.
There’s a subtlety that catches people. Throttling is enforced per 100ms period, so an application whose work arrives in short bursts can be throttled hard while its average CPU usage sits well under the limit. Average utilisation graphs will show plenty of headroom and the application will still be slow.
Exceeding a memory limit: death
Memory is incompressible. You can’t use a fraction of a byte. Exceed the limit and the kernel OOM-kills the container immediately.
No warning, no throttling, no grace. The container’s exit code is 137 and kubectl describe pod shows OOMKilled.
That asymmetry is the whole point of this post. A CPU limit set too low degrades performance in a way that’s hard to see. A memory limit set too low kills your process in a way that’s obvious but frequently misdiagnosed as an application bug.
| CPU | Memory | |
|---|---|---|
| Compressible | Yes | No |
| Exceeding the limit throttles the process | Yes | No |
| Exceeding the limit kills the container | No | Yes |
| Failure appears in pod events | No | Yes |
| Failure is visible without a metric | No | Yes |
| We recommend setting a limit | Partial | Yes |
| We recommend limit equal to request | No | Yes |
The row that matters most is the fourth: a CPU limit set too low produces no event at all, so nothing tells you where the latency came from.
QoS classes fall out of these settings
Kubernetes derives a quality-of-service class from what you set, and it decides eviction order when a node runs out of memory.
Guaranteed — every container has requests equal to limits for both CPU and memory. Evicted last.
Burstable — requests are set and are lower than limits, or only some are set. Evicted after BestEffort.
BestEffort — nothing set at all. Evicted first.
This is why “we’ll set limits later” is a decision rather than a deferral. Unset means BestEffort, which means your workload is first against the wall when a node comes under memory pressure.
The advice
Always set memory requests and limits, and set them equal. Memory usage for most services is fairly predictable, an OOM kill is severe, and equal values buy you Guaranteed QoS. Base the number on observed peak plus real headroom, not on a guess and not on the average.
Set CPU requests. Think hard before setting CPU limits. The request gets you scheduled correctly. The limit caps your ability to use idle capacity that’s sitting there anyway, and it introduces throttling that’s difficult to diagnose.
The counter-argument is real: without limits, a runaway container can starve its neighbours. Which way to go depends on whether your nodes run trusted first-party workloads or a mixed multi-tenant set. On a dedicated cluster running your own services, we usually omit CPU limits and monitor. On shared infrastructure, set them, and then watch the throttling metric.
Don’t copy requests from another team’s manifest. The numbers in the YAML you inherited were right for a different workload on different hardware, if they were ever right at all. Measure your own.
Verifying
Two commands worth knowing:
# Actual usage against what you asked for
kubectl top pods --containers
# What the node has committed versus what it has
kubectl describe node <node> | grep -A 8 "Allocated resources"
The second one is the reality check. A node showing 95% of CPU requested and 10% actually used means your requests are fiction, and you’re paying for nodes you don’t need.
This material is examinable in CKAD’s largest domain and CKA’s scheduling section. The throttling behaviour, though, is one of those things you only really learn by watching a latency graph and not understanding it for a week.
Next steps
Practise it
Run the CKAD track in a real terminal
Every objective on CertLabs is graded against live system state rather than the command you typed, on a sandboxed cluster that resets between exercises. The CKAD track covers Deploy, config, probes.
Open CertLabsCertLabs is our own practice platform.
Get help
Running this in production?
We operate Kubernetes and OpenShift for clients across the EU and the Gulf, and train the teams who inherit them. Platform assessments, migrations and hands-on enablement.
Talk to us