Requests & limits

Being a good neighbor.

Beginner10 min · lesson 23 of 24
In plain terms
Requests: scheduling guarantee. Limits: cap (OOM on memory; throttle on CPU).

Lots of apps share the same machines. Each one has to leave capacity for the others. Requests and limits are the two numbers that enforce that.

First, a few plain words. A container is your app bundled with everything it needs to run, so it behaves the same wherever it lands. A cluster is a pool of machines, called nodes, that run those containers. Each container, or a small group of them, runs inside a Pod, the smallest thing Kubernetes runs and manages. Because many Pods share the same nodes, one greedy app can grab all the CPU or memory and starve its neighbors. Requests and limits stop that.

The two numbers you set

For each container you set two numbers: a request and a limit. They sound alike, but they do opposite jobs. A request is a promise the cluster makes to you. A limit is a promise you make to the cluster.

A request is what your container needs, and the scheduler treats it as reserved capacity. It sums requests on a node and only places your Pod where the remainder fits.

A limit is the most the container is allowed to use. Cross it and Kubernetes steps in; what happens depends on which resource you went over. The file below writes both numbers. It is YAML. You apply it; the cluster creates the objects.

deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
selector:
matchLabels: { app: web }
template:
metadata:
labels: { app: web }
spec:
containers:
- name: web
image: my-app:1.4
resources:
requests:
cpu: 250m # a quarter of one CPU core, reserved for this container
memory: 256Mi # 256 mebibytes, reserved
limits:
cpu: 500m # push past this and the container is slowed down
memory: 512Mi # push past this and the container is stopped and restarted
terminal
$ kubectl apply -f deploy.yaml
deployment.apps/web created

CPU and memory are treated differently, and the reason is physical. CPU can be sliced moment by moment, so a container that passes its CPU limit is simply slowed down. That's called throttling: the app doesn't die, it runs at the speed of the cap. Memory is different. Once bytes are handed out they're gone until the app frees them, so a container that passes its memory limit gets stopped and started fresh.

terminal
$ kubectl top pods
NAME CPU(cores) MEMORY(bytes)
web-7d9f8c6b5-4xk2p 12m 140Mi
web-7d9f8c6b5-q8m7n 9m 131Mi

That's the healthy picture, usage sitting under the limits. Push a container past its memory limit, though, and the first sign is usually a Pod that won't stay up.

terminal
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-7d9f8c6b5-4xk2p 0/1 CrashLoopBackOff 4 (18s ago) 2m
web-7d9f8c6b5-q8m7n 1/1 Running 0 2m

CrashLoopBackOff means the container keeps dying and Kubernetes keeps restarting it, waiting longer between tries. The status tells you something's wrong, not what. kubectl describe answers that; read the Last State near the top.

terminal
$ kubectl describe pod web-7d9f8c6b5-4xk2p
...
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Restart Count: 4

OOMKilled is short for out-of-memory killed: the container used more memory than its limit allowed, so Kubernetes stopped it. Exit Code 137 is the same story in numbers, what a process gets when it's force-stopped. That pair together is the tell. The fix is usually a higher memory limit or a patched leak, not a deleted limit.

Three tiers decide who dies first

OOMKilled raises a bigger question: when a whole node runs out of memory, which Pods die first? Kubernetes answers with a ranking. Every Pod falls into one of three quality-of-service classes, written QoS, and you never set it yourself. Kubernetes derives it from your requests and limits.

The rule is short. Set requests equal to limits for both CPU and memory on every container and the Pod is Guaranteed, the most protected, killed last. Set some requests or limits but not matching pairs and it's Burstable, the middle tier. Set nothing at all and it's BestEffort, first out the door when the node is squeezed. The more precisely you declare what you need, the safer your Pod sits.

terminal
$ kubectl get pod web-7d9f8c6b5-q8m7n -o jsonpath='{.status.qosClass}'
Burstable

Our web Pod comes back Burstable, and that fits: deploy.yaml set requests but higher limits, so the two don't match. Make them identical and it prints Guaranteed; leave the resources out and it prints BestEffort, the tier you least want a real app in.

Backfill defaults with a LimitRange

On a shared cluster you can't trust everyone to remember these numbers, and a container that forgets them lands in BestEffort. A LimitRange fixes that for a whole namespace: a rule that stamps default requests and limits onto any container that arrives without its own.

limits.yaml
apiVersion: v1
kind: LimitRange
metadata:
name: default-resources
namespace: default
spec:
limits:
- type: Container
defaultRequest: # request stamped on if a container sets none
cpu: 250m
memory: 256Mi
default: # limit stamped on if a container sets none
cpu: 500m
memory: 512Mi
terminal
$ kubectl apply -f limits.yaml
limitrange/default-resources created

Now test it. Start a bare Pod with no resources block, then ask for its QoS class.

terminal
$ kubectl run probe --image=nginx --restart=Never
pod/probe created
$ kubectl get pod probe -o jsonpath='{.status.qosClass}'
Burstable

Without the LimitRange that Pod would've been BestEffort. The rule caught it on the way in, stamped on the default request and limit, and lifted it to Burstable, off the front of the kill list. Sane defaults for everyone, no nagging required.

Why this is worth the effort

Set both numbers early, because three things depend on them. Scheduling: with requests, the scheduler only places Pods on nodes that have room, so you avoid Pending Pods and overpacked nodes. Autoscaling: the Horizontal Pod Autoscaler (HPA) adds copies when load climbs and measures usage as a percentage of the request, so with no request it has no baseline and does nothing. Stability: a memory limit stops one container from taking the node down.

There's a balance to strike. Set requests too low and the scheduler overpacks nodes, so apps fight over scraps; set them too high and you pay for room you never touch. A safe rule: always set requests, always set a memory limit, and go easy on tight CPU limits, since throttling a healthy app just to hit a number causes more grief than it prevents.

Request vs limit
Two numbers per container
both go under resources in the Pod spec
request
the floor, guaranteed and reserved
places the Pod on a node with room; the autoscaler measures usage against it
limit
the ceiling, a hard cap
over memory the container is killed (OOMKilled); over CPU it is throttled
Requests place your app and let it scale; limits stop one container from taking down the machine.

Requests are what the scheduler uses to place pods; limits are the ceiling the runtime enforces. A pod without requests can pack densely until noisy neighbors appear. Prefer honest requests based on observed usage rather than cargo-cult numbers.

CPU limits throttle; memory limits OOM-kill. Those feel different in production. Watch for CrashLoop caused by memory limits that are too tight versus CPU throttling that just makes the app slow.

Suppose one team omits limits on a shared node pool: their leak becomes everyone's incident. Quotas and LimitRanges exist to make good-neighbor defaults enforceable.

Try this

Run a pod with explicit CPU/memory requests and limits, then inspect what the PodSpec stored and how the node allocated it.

terminal
$ kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: sized
spec:
containers:
- name: c
image: nginx:1.27
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
EOF
pod/sized created
$ kubectl get pod sized -o jsonpath='{.spec.containers[0].resources}{"\n"}'
{"limits":{"cpu":"200m","memory":"128Mi"},"requests":{"cpu":"100m","memory":"64Mi"}}
$ kubectl describe pod sized | Select-String -Pattern 'Limits|Requests|QoS' -Context 0,3
QoS Class: Burstable
Limits:
cpu: 200m
memory: 128Mi
Requests:
cpu: 100m
memory: 64Mi
$ kubectl delete pod sized
pod "sized" deleted

Takeaway

Requests place pods; limits cap them. Set both from real usage, remember memory over-limit kills while CPU throttles, and enforce defaults with LimitRange/Quota on shared clusters.

The riskiest Pod is the one with nothing set
A container with no requests and no limits is BestEffort, the first thing a node kills when memory runs short, even if it isn't the one using the most. It also leaves the scheduler and autoscaler nothing to measure. On anything you care about, set at least modest requests and a memory limit, and let a LimitRange enforce it across the namespace.

Want to feel this rather than just read about it? Deploy the file above to a small local cluster with kind or minikube (both run a tiny cluster on your laptop), run kubectl top pods, and watch usage sit under your request. Then cut the memory limit to something tiny like 20Mi, apply again, and watch the Pod flip to OOMKilled in seconds.

Quick check
01A Pod's container sets a memory request of 256Mi but no limits at all. Its node later runs out of memory. Compared with a neighbor Pod that set no requests or limits, which is more likely to be killed first?
Incorrect — No. The QoS ranking still separates them.
Incorrect — The opposite: a request moves you up the protection ladder.
Correct — Nothing set is BestEffort, first to be evicted; a request lifts you to Burstable.
Incorrect — No. Under pressure the kubelet evicts Pods, starting with BestEffort.
02A container goes over its CPU limit. What does Kubernetes do, and why is it handled differently from going over a memory limit?
Incorrect — That is the memory behavior; CPU is throttled instead because it can be reclaimed continuously.
Incorrect — Crossing a CPU limit does not evict the Pod; it just caps the container's speed.
Incorrect — CPU limits are enforced; the container is actively throttled to the cap, not ignored.
Correct — CPU is compressible so it is throttled, while over-memory forces an OOMKilled restart.
03A LimitRange in the default namespace sets defaultRequest cpu 250m / memory 256Mi and default (limit) cpu 500m / memory 512Mi. You run a bare Pod with no resources block. What QoS class does it get?
Incorrect — The LimitRange stamps defaults onto the container before it is admitted, so it never stays BestEffort.
Incorrect — The stamped request (250m/256Mi) and limit (500m/512Mi) differ, so it is not Guaranteed.
Correct — some requests and limits are present but do not match, which is the definition of Burstable.
Incorrect — It is not rejected; the LimitRange backfills the missing numbers so the Pod starts normally.

Related