Kubernetes Resource Requests, Limits, and QoS Classes
Concept
Every container in a Kubernetes Pod can declare two numbers per resource (CPU, memory): a request — what the scheduler reserves for it — and a limit — the ceiling the kubelet enforces once it's running. The two numbers do different jobs at different points in the Pod's lifecycle. The scheduler only ever looks at requests: it places a Pod on a node only if the sum of that node's already-scheduled requests plus the new Pod's requests stays under the node's allocatable capacity, which is what guarantees a Pod is never scheduled onto a node with less capacity than it asked for. The limit is enforced later, continuously, by the kubelet on the node itself, and it enforces the two resource types differently because they behave differently under contention: CPU is a compressible resource, so a container that hits its CPU limit is throttled — it keeps running, just slower — while memory is incompressible, so a container that exceeds its memory limit gets OOMKilled outright by the kernel.
From the relationship between a Pod's requests and limits, Kubernetes derives a Quality of Service (QoS) class, and that class — not anything set explicitly — is what determines eviction order when a node runs low on resources. Guaranteed: every container's request equals its limit, for every resource; the Pod gets the strongest eviction protection because Kubernetes can see an exact reservation with no slack. Burstable: at least one container sets a request lower than its limit; the Pod is scheduled on its (lower) request but can use up to its limit if the node has spare capacity, at the cost of being evicted before Guaranteed Pods if the node comes under memory pressure. BestEffort: no requests or limits are set at all; the Pod has no reservation, gets whatever's left over, and is the first class evicted under any pressure at all.
Tradeoffs
| QoS class | Setup | Benefit | Cost |
|---|---|---|---|
| Guaranteed (request = limit) | Every container sets both, equal | Strongest scheduling and eviction guarantee; predictable performance | No bursting — a workload with genuinely variable load either over-provisions for its peak (wasted spend) or gets throttled/killed at its floor |
| Burstable (request < limit) | Request set below limit | Can absorb spikes using a node's spare capacity; better bin-packing across the cluster | Evicted before Guaranteed Pods under memory pressure; CPU throttling during a burst is silent unless explicitly monitored |
| BestEffort (no request/limit) | Neither set | Simplest to write; never blocks scheduling on capacity | First evicted under any pressure at all; no throttling/OOM protection whatsoever — genuinely unsuitable for anything the caller depends on being up |
The real tradeoff isn't "which class is best" — it's how much slack (headroom between request and limit) to carry for a given workload's actual variance, and that number only comes from observed usage, not a guess.
When to use / when not to
- Use Guaranteed for workloads where a missed SLA is expensive and load is genuinely predictable — payment processing, anything on a synchronous request path with a tight latency budget.
- Use Burstable for the common case: a service whose load varies (daily traffic curve, occasional batch spikes) where over-provisioning for the peak wastes real money most of the time.
- Reserve BestEffort for genuinely disposable work — best-effort batch jobs, spot-style background processing — never for anything a user-facing request depends on.
- Don't leave requests/limits unset "for now" on a production workload; that default is BestEffort, which is rarely what was intended.
Common pitfall
Copying requests and limits from a tutorial or another team's manifest without profiling the workload's actual usage, then discovering the gap only in production: requests set too low pack more Pods onto a node than it can actually sustain under real load, so CPU gets silently throttled (latency degrades without an obvious error) or memory gets OOMKilled (the Pod restarts, which looks like a crash, not a sizing problem). The fix isn't a bigger limit by default — it's measuring actual usage under real load and setting requests close to that observed baseline, with limits set from observed peak plus margin, not copied from somewhere else.
Engineering Lens
The request/limit split is a clean instance of a general resource-management pattern: separate "what you're guaranteed" from "what you're allowed to burst into," and let the difference between them be a deliberate, measured decision rather than a default nobody revisited. The stronger question in a review isn't "did we set requests and limits" — it's "do these numbers come from observed usage, and do we know what happens to this specific workload when the node it's on comes under memory pressure." That's the same instinct as sizing a connection pool or a thread pool: the failure mode under contention has to be a known, chosen outcome, not a surprise discovered during an incident.
Sources
- Configure Quality of Service for Pods — Kubernetes Documentation
- Resource Management for Pods and Containers — Kubernetes Documentation
- The Case for Kubernetes Resource Limits: Predictability vs. Efficiency — Kubernetes Blog