Set resource requests and limits
Most resource advice says to match both limits to the request on every container and call the pod Guaranteed. Follow it here and you introduce latency you never needed. The right default is narrower: cap memory at its request, leave CPU uncapped, land as Burstable. A request is what the scheduler reserves, a limit the ceiling. Treat them the same and you ignore that CPU and memory fail differently.
Tip
Memory limit equal to the request, no CPU limit, Burstable. Right for most workloads. Go beyond it only when you can name why.
Set a CPU and memory request on every container. A request-less container looks free to the scheduler, so it packs onto a full node and gets evicted under pressure, and free to the cluster autoscaler , so a pod that should trigger a scale-up silently never does.
The shape those rules produce, on every container:
resources:
requests:
cpu: "250m" # the scheduler reserves this; your guaranteed floor
memory: "256Mi"
limits:
memory: "256Mi" # equal to the request; deliberately no cpu limit
Why the CPU limit stays off
CPU is throttled, not killed. A CPU limit is an absolute CFS quota: hit it and the kernel slows the container even when the node has idle cores, latency in exchange for nothing. The request already sets a guaranteed floor, so let the pod burst into spare CPU. Add a CPU limit only to fence a proven noisy neighbour, and "proven" means you have watched it: kubectl top pod or a CPU dashboard shows the container pinning cores that its co-tenants then wait behind. Fence it just above its real peak, not at its request.
Memory is the opposite. It cannot be reclaimed by slowing a container down, so a container that grows past its request with no limit starves the node and gets its neighbours OOM-killed. Cap the limit at the request to hold it to what it reserved; if it needs more, raise both numbers rather than dropping the limit.
QoS follows from your requests
Kubernetes derives each pod's QoS class from its requests and limits, and that class decides who the kubelet evicts first under memory pressure:
| Class | When | Under pressure |
|---|---|---|
| Guaranteed | every container has requests equal to limits, CPU and memory | evicted last |
| Burstable | at least one request set, but not Guaranteed | evicted after BestEffort |
| BestEffort | no requests or limits at all | evicted first |
That combination lands you in Burstable, protected on memory and free on CPU. Use Guaranteed only when a workload demands it, such as a NUMA-aligned database that pins cores with whole-integer CPU requests equal to limits. The reason there is not the eviction ranking: the CPU Manager only hands a pod dedicated, unshared cores when it is Guaranteed with whole-integer CPU, so pinning is simply impossible in Burstable. That requirement, not eviction order, is what pulls those workloads up.
Node capacity is not the whole machine
The scheduler places against a node's allocatable capacity, not its physical size. The platform reserves CPU and memory for system daemons, holds back an eviction buffer, and caps pods per node. See Node resources and limits for the figures. Never size a pool as if every byte were schedulable.
Right-size from real usage
Guessing requests wastes money when you aim high and triggers evictions when you aim low. Measure instead. The Vertical Pod Autoscaler watches real usage and recommends requests from history. Run it in recommendation mode so it advises without moving your pods:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: web
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: web
updatePolicy:
updateMode: "Off" # recommend only, never evict
After a day or so of history, read the recommendation and copy the target numbers into your requests:
$ kubectl get vpa web -o jsonpath='{.status.recommendation.containerRecommendations[0].target}'
{"cpu":"180m","memory":"210Mi"}
Verify the QoS class
Requests and limits only matter through the QoS class they produce, so confirm it instead of assuming it:
$ kubectl get pod <pod> -o jsonpath='{.status.qosClass}'
Burstable
Burstable is the target for most workloads. BestEffort means the requests are missing and the pod is first in line for eviction. Guaranteed means every request equals its limit for both CPU and memory; since the shape here already matches memory, adding a CPU limit is all it takes to land there, which reintroduces the throttling this page tells you to avoid, unless you set out to pin cores.
Configure health probes
The probe mistakes that drop traffic on a platform that drains and replaces nodes, and how readiness, liveness, and startup probes avoid them.
Add a PodDisruptionBudget
Cap how many pods can be down during a drain so an upgrade or repair never takes your whole app offline, and read the two symptoms that mean the budget is wrong.