requests and limits look like a matched pair — same fields, same units, sitting right next to each other in a container spec — which makes it easy to assume they do roughly the same job at different thresholds. They don't. They're read by two completely different parts of Kubernetes, at two completely different times, for two completely different purposes.
Requests: what the scheduler reserves
When a pod needs to run somewhere, the scheduler looks at every node's capacity and every pod already running on it, and asks: does this node have enough unreserved capacity for this new pod? That comparison is done entirely in terms of requests — the scheduler sums up the CPU and memory requests of every pod already placed on a node and checks whether the new pod's requests fit in what's left. limits play no part in this decision at all. A pod with no requests set is, as far as the scheduler is concerned, free to place anywhere — which is exactly the BestEffort QoS case.
Limits: what the node enforces afterward
Once a pod is running, limits take over as a completely separate mechanism, enforced locally by the kubelet and the container runtime via cgroups — nothing to do with scheduling anymore. This split is the whole reason the two fields can (and often should) have different values: requests answer "where should this run," limits answer "how far can it go once it's there."
Overcommitment is normal — and sometimes risky
Because scheduling only checks requests, nothing stops the sum of every pod's limits on a node from exceeding that node's actual capacity. This is called overcommitment, and it's not a misconfiguration — it's the normal state of most real clusters, because most workloads don't use their full limit at the same time. The risk shows up specifically when several pods on the same node do burst at once: there genuinely isn't enough CPU or memory to give everyone what their limit promises, and something has to give.
What actually happens when you cross each line
This is the part worth knowing precisely, because the three outcomes are completely different:
- Using more than your request, but less than your limit: nothing — this is normal, expected headroom, not a violation of anything. The request was only ever a reservation for scheduling purposes.
- Exceeding a CPU limit: throttled, not killed. CPU limits are enforced via a quota measured over a fixed period (100ms by default) — if a container uses its entire allotted slice before the period ends, it's paused until the next one starts. This can happen in short, bursty spikes even when the container's average CPU usage over a longer window looks well under the limit, and even when the node itself has idle CPU sitting unused — throttling is enforced per-container, independent of what else is or isn't happening on the node.
- Exceeding a memory limit: the container is killed immediately (
OOMKilled, visible inkubectl describe pod) and restarted per its restart policy. There's no throttling equivalent for memory — you can't partially deny a memory allocation the way you can delay a CPU cycle, so the kernel's OOM killer just ends the process.
That asymmetry is the real argument for a common pattern: set memory requests and limits (since exceeding memory is a hard failure you want to control deliberately), but think harder before setting a CPU limit at all, since the failure mode for CPU is silent latency, not a clean restart.
A practical way to choose values
Guessing tends to go one of two predictable ways: requests set far above real usage, which wastes capacity and lets fewer pods fit per node, or requests set far below it, which crowds too many pods onto a node and starves everyone once they're all busy at once. Actual usage data — kubectl top pod for a quick look, or a metrics pipeline over days or weeks for anything that matters — beats a guess every time. Set requests near typical steady-state usage with some headroom, and treat limits as a deliberate ceiling decision, not a formula applied uniformly to every container.
Namespace-wide guardrails: LimitRange and ResourceQuota
Two more objects extend this beyond a single pod. A LimitRange sets default/min/max requests and limits for every container in a namespace — a pod that doesn't specify anything picks up the namespace default automatically, and one that asks for something outside the configured min/max is rejected at admission time. A ResourceQuota caps the total requests and limits summed across every pod in a namespace, so one team's workloads can't silently consume capacity another team was counting on. Neither is visible in an individual pod's manifest — they live in the namespace as separate objects — which is exactly why a calculation based only on what's in front of you can still be wrong in a cluster that has either configured.
Try it yourself
Kubernetes Resource Calculator computes a manifest's total requests/limits and its resulting QoS class instead of you working through the rules by hand, Kubernetes YAML Validator catches structural mistakes before kubectl apply does, and K8s Diff shows exactly how requests and limits changed between two versions of a manifest. All three run entirely in your browser.