Requests vs Limits in Kubernetes: What OOMKilled Means
6 min readBytePatterns
Requests reserve, limits enforce: why CPU over its limit is throttled but memory is OOMKilled with exit code 137, and how QoS classes pick who is evicted first.
Requests vs Limits in Kubernetes: What OOMKilled Means, in one line: a request is the reservation the scheduler uses to place a Pod, a limit is the ceiling enforced while it runs, and OOMKilled means a container used more memory than it was allowed and the kernel killed it. CPU past its limit is throttled and keeps running; memory past its limit is killed. When a whole node runs short, a second mechanism, ranked by requests, decides who goes first. Facts come from the Kubernetes documentation listed at the end, as of October 2026.
| Situation | Who acts | What you see |
|---|---|---|
| Requests do not fit any node | the scheduler | Pending, insufficient cpu or memory |
| Usage over the CPU limit | the kernel, by throttling | slower responses, no restart |
| Usage over the memory limit | the kernel's OOM killer | OOMKilled, exit code 137, a restart |
| The node runs short of memory | the kubelet, by eviction | Pod Failed, its controller replaces it |
The problem it solves
The HPA side of these two fields, utilization as a percentage of the request, is covered in Kubernetes requests, limits and the HPA. This article stays on one node and answers the questions that follow an incident:
- Why is this Pod
Pendingwhen the node looks idle? - Why did my container restart with
OOMKilled, and why not when CPU spiked? - Why was this Pod evicted and not that one?
All three come from the same split: requests are read at placement, limits at run time.
The intuition
A request is a reservation, not a measurement. The scheduler places a Pod only on a node where, for each resource, the sum of requests of the Pods there stays within what the node can allocate. Actual usage does not enter the check. An idle node can refuse a Pod whose request does not fit, and lowballed requests let a node accept Pods it cannot serve. If you set a limit and no request, Kubernetes copies the limit as the request.
A limit is enforced, and the method depends on the resource.
- CPU is compressible. A container that wants more than its CPU limit is throttled: it gets less CPU time, gets slower, and keeps running. Without a CPU limit it can use idle CPU beyond its request.
- Memory is not. The documentation says memory limits are enforced reactively by the kernel: a container that allocates past its limit becomes a candidate for termination and, if it keeps going, is killed.
kubectl get podshowsOOMKilled, and the container'slastStaterecordsreason: OOMKilledwithexitCode: 137, which is 128 plus signal 9,SIGKILL. The kubelet restarts it according to the Pod'srestartPolicy; repeated kills turn intoCrashLoopBackOff.
Node pressure is a different event. A container can be OOM-killed by its own limit while the node is fine. When the node itself runs low on memory, the kubelet evicts whole Pods. The documented ranking is: Pods whose usage exceeds their requests first, then by priority, then by how far usage exceeds the request. Evicted Pods are marked Failed and replaced by their controller, possibly on another node; an OOM-killed container is restarted in place.
QoS classes summarize the settings:
Guaranteed: every container has CPU and memory requests equal to its limits. These Pods are never evicted because of another Pod's consumption.Burstable: at least one request or limit, but notGuaranteed. Evicted afterBestEffort, early if over their requests.BestEffort: no requests or limits at all. With no request, usage always exceeds it, so they go first.
Watch it run
The lesson animation shows the request doing its other job. One bar per Pod is its CPU use as a percentage of the 250m request, against a 60% target. When traffic pushes the bars to 90%, the HPA asks for more Pods, and the new ones wait Pending because no node has 250m of CPU left to reserve. Node autoscaling adds a node and they schedule. The scheduler counts requests; the bars are usage.
Requests, Limits & HPA
Step 1 of 12
One bar per Pod: its CPU use as a percentage of its request, 250m. The HPA's target is 60%.
The same interactive animation as the lesson — step through it with the controls.
The code
The lesson's container: requests for both resources and a memory limit, with no CPU limit:
# illustrative: the lesson's api container
resources:
requests:
cpu: 250m # the scheduler reserves this
memory: 256Mi
limits:
memory: 512Mi # past this, the container can be OOMKilled
# a later kubectl get pod -o yaml after a kill shows:
# lastState:
# terminated:
# exitCode: 137
# reason: OOMKilled
A toy model, not the scheduler or the kernel. Placement uses only requests, so a 1000m node takes four 250m Pods and the fifth is Pending. At run time, only limits act: CPU over the request with no CPU limit runs at full speed, memory over the limit is killed with exit code 137, and CPU over a CPU limit is throttled:
NODE = {"cpu": 1000, "memory": 2048} # allocatable: millicores, MiB
API = {"requests": {"cpu": 250, "memory": 256}, "limits": {"memory": 512}} # the lesson's
def schedule(pods, node=NODE):
"""Toy model of the scheduler's resource check: only requests count."""
used, placed = {"cpu": 0, "memory": 0}, []
for name, spec in pods:
req = spec.get("requests", {})
if all(used[r] + req.get(r, 0) <= node[r] for r in node):
for r in node:
used[r] += req.get(r, 0)
placed.append(name)
else:
print(name, "Pending: Insufficient", [r for r in node if used[r] + req.get(r, 0) > node[r]])
return placed, used
placed, used = schedule([(f"api-{i}", API) for i in range(1, 6)])
print(placed, used)
def enforce(spec, cpu_use, mem_use, restarts=0):
"""Toy model of run-time enforcement: limits act, requests do not."""
lim = spec.get("limits", {})
if "memory" in lim and mem_use > lim["memory"]:
return {"state": "OOMKilled", "exitCode": 128 + 9, "restarts": restarts + 1}
cpu = min(cpu_use, lim.get("cpu", cpu_use)) # over a CPU limit: throttled
return {"state": "Running", "cpu": cpu, "throttled": cpu < cpu_use}
print(enforce(API, cpu_use=600, mem_use=480)) # past the cpu request, no cpu limit
print(enforce(API, cpu_use=200, mem_use=530)) # past the 512Mi memory limit
capped = {"requests": {"cpu": 250, "memory": 256}, "limits": {"cpu": 300, "memory": 512}}
print(enforce(capped, cpu_use=600, mem_use=480))
# api-5 Pending: Insufficient ['cpu']
# ['api-1', 'api-2', 'api-3', 'api-4'] {'cpu': 1000, 'memory': 1024}
# {'state': 'Running', 'cpu': 600, 'throttled': False}
# {'state': 'OOMKilled', 'exitCode': 137, 'restarts': 1}
# {'state': 'Running', 'cpu': 300, 'throttled': True}
The first Running line assumes the node has idle CPU to give; without a CPU limit the container competes for it, weighted by its request.
Next, the QoS classifier for three specs, including the subtle one: a container with limits only gets requests copied from them, so it is Guaranteed. Then the overcommit story. Eight web Pods request 128Mi but really use 200–300Mi; the scheduler happily packs them next to a database, and the node is asked for more memory than it has:
def with_defaults(c):
"""A limit with no request: the request is copied from the limit."""
req = dict(c.get("limits", {}), **c.get("requests", {}))
return {"requests": req, "limits": c.get("limits", {})}
def qos(containers):
"""Toy model of the QoS rules for container-level resources."""
cs = [with_defaults(c) for c in containers]
if all(c["limits"].get(r) and c["limits"].get(r) == c["requests"].get(r)
for c in cs for r in ("cpu", "memory")):
return "Guaranteed"
if any(c["requests"] or c["limits"] for c in cs):
return "Burstable"
return "BestEffort"
print(qos([API])) # Burstable
print(qos([{"limits": {"cpu": 500, "memory": 512}}])) # Guaranteed
print(qos([{}])) # BestEffort
def eviction_order(pods):
"""Toy model of the documented ranking under memory pressure:
usage over request first, then lower priority, then most over request."""
def key(p):
over = p["mem"] - p["req"]
return (over <= 0, p["priority"], -over)
return [p["name"] for p in sorted(pods, key=key)]
lowballed = {"requests": {"cpu": 100, "memory": 128}, "limits": {"memory": 512}}
db = {"requests": {"cpu": 200, "memory": 512}, "limits": {"memory": 512}}
placed, used = schedule([(f"web-{i}", lowballed) for i in range(1, 9)] + [("db-0", db)])
print(len(placed), "pods fit: requests", used["memory"], "MiB of", NODE["memory"])
usage = dict(zip(placed, [300, 280, 260, 250, 240, 230, 220, 200, 400])) # real MiB
print(sum(usage.values()), "MiB wanted: more than the node has")
pods = [{"name": n, "req": (512 if n == "db-0" else 128), "mem": m, "priority": 0}
for n, m in usage.items()]
order = eviction_order(pods)
print(order[:3], "...", order[-1])
# Burstable
# Guaranteed
# BestEffort
# 9 pods fit: requests 1536 MiB of 2048
# 2380 MiB wanted: more than the node has
# ['web-1', 'web-2', 'web-3'] ... db-0
No single container passed its own 512Mi limit, so none was OOM-killed by its limit. The node ran out instead, and the web Pods that most exceeded their requests are evicted first. db-0, using less than it requested, goes last. Lowballed requests caused the incident; an honest request protected the database.
The complexity
The trade-off is cost against predictability:
- Requests at real usage keep scheduling honest, at the price of fewer Pods per node.
- Memory limit equal to the request makes memory predictable: a Pod never uses more than it reserved, but any spike is an
OOMKilled. - A CPU limit caps noisy neighbours and costs latency through throttling; the lesson's manifest sets a CPU request and no CPU limit.
Where it goes wrong
- Reading
OOMKilledas a node problem. If the container hit its own limit, the node may be fine. Raise the limit or fix the leak; more replicas do not change a per-container limit. - Reading eviction as
OOMKilled. An evicted Pod isFailedand replaced, often elsewhere; check the node's memory pressure, not the container's limit. - Requests copied from a guess. Requests far below usage overcommit the node, as in the toy; far above it waste capacity.
- A tight CPU limit on a latency-critical service. Throttling shows up as slow responses, not restarts.
When it shows up in interviews
As debugging: "a Pod restarts with exit code 137, why?" The expected answer is the memory limit, OOMKilled, kubectl describe pod for the last state, and the difference from eviction. A design variant asks how you would size requests and which QoS class a database should get.
How to say it in an interview
"Requests are for the scheduler: a Pod fits a node when the sum of requests does. Limits are enforced at run time: CPU over its limit is throttled, memory over its limit is OOM-killed, which is exit code 137, and the kubelet restarts the container per restartPolicy. Under node memory pressure the kubelet evicts Pods that exceed their requests first, which is why BestEffort goes before Burstable and Guaranteed. I set requests from observed usage, a memory limit close to the request, and usually no CPU limit on latency-sensitive services."
The manifest and HPA exercise are in requests, limits and HPA; restarts caused by probes instead of memory are in which probe restarts the Pod.
Frequently asked questions
What does OOMKilled mean in Kubernetes?
The container used more memory than its limit, and the kernel killed it. The container's last state shows reason: OOMKilled and exitCode: 137, which is 128 plus SIGKILL. The kubelet restarts it according to the Pod's restartPolicy, and repeated kills appear as CrashLoopBackOff. Raise the limit or fix the memory growth.
What is the difference between requests and limits in Kubernetes?
A request is a reservation: the scheduler places a Pod only where the sum of requests fits the node. A limit is a ceiling enforced while the container runs: past its CPU limit it is throttled, past its memory limit it can be OOM-killed. A limit without a request has the request copied from it.
Does exceeding a CPU limit kill the container?
No. CPU is compressible, so a container that wants more than its CPU limit is throttled: it gets less CPU time and runs slower, with no restart. Only memory over its limit leads to an OOM kill. Throttling shows up as latency, which makes a tight CPU limit hard to spot.
Sources
- Resource Management for Pods and Containers — Kubernetes Documentation
- Assign Memory Resources to Containers and Pods — Kubernetes Documentation
- Pod Quality of Service Classes — Kubernetes Documentation
- Node-pressure Eviction — Kubernetes Documentation