Requests & limits

Being a good neighbor.

Beginner10 min · lesson 23 of 24
In plain terms
Requests and limits are telling the landlord how much space you need and promising not to blast music past a set volume — so everyone in the building gets along.

Renting a room in a shared house comes with a couple of unwritten rules. You tell the landlord how much space you actually need, and you promise not to blast music at 3 a.m. so the other tenants can sleep. A Kubernetes cluster runs on the same kind of courtesy. Lots of apps share the same machines, and each one has to be a decent housemate.

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 to run, and Kubernetes guarantees it. Think of it as the floor: the landlord holding your room even on nights you're away. The scheduler, the part that picks which machine a new Pod lands on, sums every Pod's request and only places yours on a node with room to spare. So a request reserves capacity and lands your Pod where it fits.

A limit is the most your container is allowed to use, the ceiling. Cross it and Kubernetes steps in, though what happens next depends on which resource you went over. The file below shows both numbers written out. It's a YAML file, a plain-text way to describe what you want Kubernetes to build. Nothing runs it by hand; you hand the file to the cluster and it does the rest.

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

Setting these two numbers is one of the best early habits, because three things quietly depend on it. Scheduling: with requests in place, the scheduler puts Pods only on nodes that really have room, so you avoid Pending Pods and overpacked nodes that tip over. Autoscaling: the Horizontal Pod Autoscaler, or HPA, adds copies when traffic climbs and measures load as a percentage of the request, so no request means no baseline and it silently does nothing. Stability: a memory limit is a seatbelt for the node.

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