Skip to content
BytePatterns

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.

SituationWho actsWhat you see
Requests do not fit any nodethe schedulerPending, insufficient cpu or memory
Usage over the CPU limitthe kernel, by throttlingslower responses, no restart
Usage over the memory limitthe kernel's OOM killerOOMKilled, exit code 137, a restart
The node runs short of memorythe kubelet, by evictionPod 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 Pending when 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 pod shows OOMKilled, and the container's lastState records reason: OOMKilled with exitCode: 137, which is 128 plus signal 9, SIGKILL. The kubelet restarts it according to the Pod's restartPolicy; repeated kills turn into CrashLoopBackOff.

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 not Guaranteed. Evicted after BestEffort, 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 OOMKilled as 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 is Failed and 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