Requests and Limits
A request is a scheduling reservation; a limit is an enforced ceiling. CPU and memory behave completely differently when you reach the ceiling, and that difference is the lesson.
The question, the obvious approach, and why it breaks
Every lesson starts where the work starts: an operational problem, a first attempt that is entirely reasonable, and the way production disagrees with it.
What do requests and limits actually control, and why does exceeding a CPU limit feel nothing like exceeding a memory limit?
Several workloads share a machine. Something has to decide how much each is promised, what happens when one wants more, and which one loses when the machine runs out.
Requests and limits are a minimum and a maximum for the same thing. Set the request to what it usually uses, the limit to a comfortable multiple, and the platform sorts out the rest.
They are consumed by two different systems at two different times. The scheduler reads the request once, at placement. The kernel enforces the limit continuously, at runtime. Nothing on the node ever looks at the request to decide whether to let a process run.
- They are consumed by two different systems at two different times. The scheduler reads the request once, at placement. The kernel enforces the limit continuously, at runtime. Nothing on the node ever looks at the request to decide whether to let a process run.
- Treating them as one axis hides the crucial asymmetry: over a CPU limit the process is slowed down and keeps running; over a memory limit it is killed (OOMKilled: Over the Memory Limit, CPU Throttling: The Latency With No Error).
- A request with no limit means the workload can consume everything the node has spare — which works beautifully until the node is contended and the neighbours suffer.
- A limit with no request — or an unset request — means the scheduler places blind, so the node is oversubscribed in a way nobody declared and nobody can see (How Resource Settings Go Wrong).
- The ratio between request and limit decides the pod's QoS class, which decides who the kubelet evicts first when the node itself runs short. Most teams set that by accident.
What is actually happening
Underneath the tooling, which is the part that survives a change of tool.
- Request is a claim on the scheduler's ledger. The sum of requests on a node cannot exceed its allocatable capacity, and that arithmetic is the only thing placement uses (The Scheduler, and Why a Pod Is Pending).
- CPU request additionally becomes the container's relative weight under contention. When several containers want the CPU at once, the kernel divides time in proportion to their requests — so a request is also a share, not just a reservation.
- CPU limit becomes a quota per scheduling period. When the container has used its quota for the current period it is descheduled until the next one begins. It is not killed, not signalled, and not informed. It simply stops running for a while (CPU Throttling: The Latency With No Error).
- Memory request is only a scheduling number. There is no such thing as a memory share — memory is either allocated to you or it is not.
- Memory limit becomes a hard cgroup ceiling. An allocation that would exceed it triggers reclaim, and when reclaim cannot free enough the kernel kills a process in that cgroup (OOMKilled: Over the Memory Limit).
- CPU is compressible: you can take it away and give it back, and the workload gets slower. Memory is incompressible: you cannot take back memory a process is using, so the only enforcement available is termination. Every behavioural difference follows from that one property.
- QoS class follows from the settings. Requests equal to limits on every resource gives
Guaranteed; requests set and lower than limits givesBurstable; nothing set givesBestEffort. Under node memory pressure the kubelet evictsBestEffortfirst, thenBurstable, andGuaranteedlast.
Four cells, two very different halves
Everything operational about this topic is in one table. Read it as two rows that behave nothing alike, rather than as two columns of the same setting.
| Request | Limit | What happens at the ceiling | |
|---|---|---|---|
| CPU | Scheduling reservation, and the container's share of CPU time under contention | A quota per scheduling period, enforced by the kernel | Descheduled until the next period. Slower, still alive, no error anywhere (CPU Throttling: The Latency With No Error) |
| Memory | Scheduling reservation only — memory has no notion of a share | A hard cgroup ceiling on resident memory | Reclaim, then the kernel terminates a process in the cgroup (OOMKilled: Over the Memory Limit) |
| Set neither | Scheduler places blind; the node is oversubscribed invisibly | No enforcement; the container can take whatever is free | The node degrades or the node-level OOM killer chooses a victim you did not |
| Set both, equal | Fully reserved; Guaranteed QoS | Ceiling equals reservation, so there is no burst | Predictable: the workload gets what it asked for and is evicted last |
What the scheduler sees versus what the kernel enforces
The most common misunderstanding is that these two numbers are read by the same component. They are not, and the gap between them is exactly where oversubscription lives.
A node can be fully committed on requests while idle in usage, or fully busy in usage while nominally uncommitted. Both are normal. Only the first affects scheduling; only the second affects your users.
requests.cpu: 500m -> "it gets half a core" limits.cpu: 1000m -> "it can use up to one core" requests.memory: 512Mi -> "it gets 512Mi" limits.memory: 1Gi -> "it can grow to 1Gi if needed" # one system, one meaning, both ends soft
SCHEDULER, once, at placement: reserves 500m CPU + 512Mi on a node ignores both limits entirely KERNEL, continuously, at runtime: cpu quota = 1000m per period -> over it: descheduled memory max = 1Gi -> over it: process killed ignores both requests for enforcement (CPU request survives as the contention share)
Once you see that placement and enforcement are different systems reading different fields, the confusing behaviours stop being confusing: a node can be "full" with idle CPUs because requests are committed, and a container can be killed on a node with free memory because its own ceiling was reached.
The one setting reasonable people disagree about
Memory limits have a clear answer: set them, and set them equal to the request. CPU limits do not. The argument is real, both sides have production experience behind them, and the right answer depends on whether your cluster is shared and how latency-sensitive the workload is.
Memory limit is settled. What about CPU?
when Single-team cluster, latency-sensitive services, and you trust the requests to be roughly right. Contention is resolved by shares, which is what shares are for.
cost A runaway or newly regressed workload can consume all spare CPU on its node, degrading neighbours in a way that is hard to attribute. You are trading isolation for latency.
when Shared or multi-tenant clusters where a noisy neighbour is a real risk, and some burst headroom is still wanted.
cost Bursty workloads throttle at the ceiling, which shows up as tail latency with no error and no log line — the most misdiagnosed symptom in this module (CPU Throttling: The Latency With No Error).
when You need Guaranteed QoS, strictly predictable per-pod cost, or a hard tenancy boundary.
cost No burst at all. Anything spiky pays for its peak continuously, and you will be sizing for the peak on every replica (Overprovisioning).
when Genuinely disposable batch work where a kill and a retry cost nothing.
cost BestEffort QoS: evicted first under node pressure, and invisible to capacity planning because it made no claim (Building a Capacity Model).
How to do it properly
Most important first.
- Set memory request and memory limit to the same value for anything you care about. Memory has no useful burst behaviour — bursting past the request just means the kill happens later and less predictably.
- Set the CPU request from observed usage, and treat it as the share you want the workload to win under contention (Building a Capacity Model).
- Decide the CPU limit deliberately rather than by habit. It is the one genuinely contested setting in this lesson, and both answers are defensible.
- Derive numbers from a profile under realistic load, not from the largest number seen once (How Resource Settings Go Wrong).
- Make the QoS class an intentional choice for critical workloads, so eviction order under node pressure is something you chose rather than something you inherited.
- Revisit the numbers when the workload changes shape. Requests are a claim about behaviour, and behaviour drifts with every release (Capacity Management).
How much can this affect
Every production change has a blast radius. Stated as a scale so it is comparable between changes rather than adjectival — and paired with what actually contains it, because a wide scope with a real containment mechanism is a different situation from a wide scope with none.
A wrong value in one workload's template is contained to that workload — but every replica shares it, so all of them hit the same ceiling under the same traffic. It reaches node and zone scope when an unlimited or oversized workload starves its neighbours, or when the value comes from a shared service template that every team inherits.
What can go wrong
- Memory limit set below real peak usage: the container is killed under exactly the load you most wanted it to survive.
- CPU limit set low on a workload that is bursty by nature: constant throttling, presenting as tail latency with no error and no obvious cause.
- No requests at all: the scheduler packs the node blind, and everything on it degrades together the first time real traffic arrives.
- Requests copied from another service's manifest, so the cluster is sized for a workload nobody has.
- Requests inflated for safety across every service, producing a cluster that is expensive and mostly idle while still reporting scheduling pressure (Overprovisioning).
- A runtime that reads the machine's CPU count rather than the cgroup quota, and sizes its thread pools for hardware it will never be allowed to use.
- "The limit is the amount the container gets." The request is closest to what it is promised. The limit is only the point where enforcement starts.
- "Setting a high limit is harmless." A high memory limit lets a leak grow until it destabilises the node instead of killing one container early (OOMKilled: Over the Memory Limit).
- "CPU and memory work the same way." They are opposites at the ceiling: one is throttled, the other is killed. This is the single most useful fact in the module.
- "BestEffort just means unconfigured." It also means first to be evicted when the node is under memory pressure — an availability decision made by omission.
Operating it
Evidence is the signal, not the intention. Rollback is sometimes 'you cannot, and that is the point'.
- Peak memory usage per container against its limit, over a window that includes your worst traffic — a margin you can name, not a hope.
- Throttling counters at zero, or at a level you have deliberately accepted, for latency-sensitive services (CPU Throttling: The Latency With No Error).
- Sum of requests against allocatable capacity per node, showing the headroom a rolling update needs (Headroom).
- QoS class per critical workload matches what you intended, checked in the live object rather than the template.
- No
OOMKilledcontainer terminations outside deliberate load tests.
- Changing requests or limits replaces every pod, because both are part of the pod template. There is no in-place adjustment — a "small tuning change" is a full rollout with all of a rollout's risk (Apply Is Not Running).
- Lowering a memory limit is the dangerous direction and it is not reversible in the moment: pods are killed as they cross the new ceiling, and restoring the old value is itself another rollout.
- Raising a request can make the workload unschedulable, so the rollout stalls with pods
Pendingand the old ones still serving. That is a recoverable state, but only if you notice it.
- Automate the measurement: usage against request and limit per container, visible without anyone running a command.
- Automate the guardrails: policy that rejects a workload with no memory limit, or with a request that is obviously copied, is cheap and catches the common mistake (Policy as Code).
- Be careful automating the values themselves. A recommender that rewrites requests will also replace pods to do it, so it must respect disruption budgets and rollout windows or it becomes a scheduled outage (The Automation Trap).
- Guaranteed QoS gives predictable behaviour and the last place in the eviction queue, at the cost of reserving capacity the workload usually is not using.
- No CPU limit gives better latency and less predictable neighbours; a CPU limit gives predictable neighbours and worse tail latency for bursty work.
- Tight requests raise density and lower the room available for failover and rollouts. Cost and resilience trade directly here, and the exchange rate is your cluster's (Cost Awareness).
Where this applies
This domain is unusually tool- and organisation-dependent. These labels say what each claim is specific to, and what a different platform, provider or organisation does instead.
- KUBERNETES-SPECIFICThe request/limit split, the QoS classes and eviction order are Kubernetes. On a VM you choose an instance size, which is request and limit at once and cannot be exceeded — the failure surfaces as the whole machine running out rather than one container being killed. A PaaS gives you a plan tier with the same fused semantics. Serverless removes the choice: the platform sets a memory ceiling and often bills against it, so "over the limit" is a request failure rather than a process kill.
- SIMPLIFIEDDescribed in terms of cgroup behaviour without distinguishing cgroup v1 from v2, and ignoring the kernel's reclaim path before an OOM kill. The observable behaviour — throttle for CPU, kill for memory — is the same on both.
Where the depth lives
This domain teaches delivery and operation, and hands the mechanism off to the domain that owns it.