Requests schedule, limits enforce
resources:
requests:
cpu: "250m" # reserved for scheduling; a share under contention
memory: "256Mi" # reserved for scheduling
limits:
cpu: "500m" # hard ceiling: the container gets throttled
memory: "512Mi" # hard ceiling: the container gets OOM-killedThe request is a promise from the cluster: the scheduler only places the Pod on a node whose unreserved capacity covers the sum of its requests, and the Pod stays Pending until one does. It is about placement, not actual usage; a node can look idle and still be “full” because everything on it requested more than it uses.
The limit is a cap enforced on the node by the kernel through cgroups. If you set a limit and no request, Kubernetes copies the limit into the request.
Units
- CPU is measured in cores:
1is one core,0.5is half, and100mis 100 millicores (a tenth of a core). The smallest value is1m. - Memory is bytes, written with a suffix:
256Mi(mebibytes, 220) or256M(megabytes, 106).
Watch the case: a lowercase m on memory means “milli,” so memory: 400m is 0.4 bytes, almost certainly not what you meant. You wanted 400Mi.
CPU is throttled, memory is killed
The two limits fail differently because the two resources behave differently. CPU time can be handed out in slices, so a container over its CPU limit is simply made to wait: it slows down but keeps running. Memory can’t be taken back from a running process, so a container over its memory limit is killed by the kernel’s OOM killer. Kubernetes reports it as OOMKilled with exit code 137 (see troubleshooting and exit codes).
You can see both mechanisms with plain Docker, which sets the same cgroup limits that the kubelet does:
# memory: allocate past the limit -> killed
docker run --name hog --memory=32m --memory-swap=32m alpine sh -c 'tail /dev/zero'
docker inspect hog --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
# true 137
# cpu: spin on a 0.25-core limit -> throttled, not killed
docker run --rm --cpus=0.25 alpine sh -c \
'timeout 3 sh -c "while :; do :; done"; grep throttled /sys/fs/cgroup/cpu.stat'A CPU limit that’s too low is the quiet one: no restart, no event, just a slower app and latency that doesn’t match the dashboards. A memory limit that’s too low announces itself with restarts and a CrashLoopBackOff.
QoS classes and eviction
From the requests and limits you set, Kubernetes assigns each Pod a Quality of Service class. When a node runs short on memory or disk, the kubelet uses the class to pick which Pods to evict.
- Guaranteed — every container has a CPU and a memory request and limit, and in each container the request equals the limit. Evicted last.
- Burstable — some request or limit is set, but the Pod doesn’t meet the Guaranteed rules. This is the most common class.
- BestEffort — no CPU or memory requests or limits at all. First in line for eviction.
Under pressure the order is BestEffort, then Burstable, then Guaranteed, and within that only Pods using more than their requests are candidates. So the request is also your eviction protection: a Pod using less than it requested is the last to go.
kubectl get pod <pod> -o jsonpath='{.status.qosClass}'
kubectl describe node <node> # "Allocated resources": requests/limits vs capacity
kubectl top pod # actual usage (needs metrics-server)Setting sensible values
- Set the memory request equal to the limit. Memory can’t be reclaimed, so promising less than you can use only moves the failure to a worse moment: a node-level eviction instead of a clear OOM on your own container.
- Set the CPU request from observed typical usage. It’s a scheduling weight and a floor under contention, not a cap.
- A CPU limit is a judgment call. It protects neighbours from a runaway loop, but it also throttles legitimate bursts. Many teams set requests everywhere and CPU limits only where they need the ceiling. A missing request is the bigger problem, because autoscalers key off it (see autoscaling).
- Start from measurements (
kubectl top, your metrics), not guesses, and leave headroom above the observed peak for memory.
Namespace defaults and caps
Two namespace-level objects keep individual teams honest. A LimitRange sets per-container defaults and minimum/maximum values, injecting defaults into Pods that don’t declare any. A ResourceQuota caps the namespace total.
apiVersion: v1
kind: LimitRange
metadata:
name: container-defaults
namespace: team-a
spec:
limits:
- type: Container
defaultRequest: # applied when a container sets no request
cpu: 100m
memory: 128Mi
default: # applied when a container sets no limit
cpu: 500m
memory: 256Mi
max:
memory: 1Gi
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-a-quota
namespace: team-a
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.memory: 40Gi
pods: "50"Both are checked when a Pod is created, not on running Pods. A request that violates either is rejected with 403 Forbidden, and once a quota covers CPU or memory, every Pod in that namespace must declare them (or get them from a LimitRange) or it won’t be admitted.
The Kubernetes linter flags manifests missing requests and limits, and Pods & Deployments shows them in context.