How to Choose requests and limits — Rules That Hold Up in Operations

4 min read

That requests are the scheduling reservation and limits are the ceiling was covered in K8s Intermediate #4. But the question in the field isn’t the concept — it’s “so what number do I write?” This post lays out working rules for choosing the values, and the shapes of the incidents when they’re wrong. It’s also the homework deferred from the Pod Pending and HPA posts.

requests — start near the measured p95 #

Requests are a promise to the scheduler and the share you’re guaranteed under node contention. So the basis is measurement, not hope.

  • Observe 1–2 weeks of usage under normal load, then start CPU between the mean and p95, and memory near p95-to-peak. Memory gets the more conservative number because CPU shortage only slows you down, while memory shortage kills the process.
  • If you must start from a guess, start small and raise it. The reverse (start big, trim later) rarely happens in practice, and over-reservation fossilizes.
  • Attach VPA in recommendation (Off/Initial) mode and you get measurement-based suggestions for free. It’s a useful sizing tool even if you never enable auto mode.

What happens when requests run far above reality appeared in Pod Pending, item 2: cluster usage is low, reservations are full, scheduling jams, and node bills are paid on reservations. Oversized requests are an invisible fixed cost.

CPU limits — omitting them can be the sane default #

CPU limits are the contested item, and the contention is throttling. A CPU limit is a cgroup time quota: a container that hits it is force-paused until the next period. That throttling is a regular cause of p99 latency spikes — including the counterintuitive case where average usage sits well below the limit but a multithreaded app’s momentary burst exhausts the period’s quota.

Workable rules:

  • If requests are set properly, consider omitting CPU limits as the default. CPU is a compressible resource: without limits, contention still divides fairly by request ratio, and using spare CPU is pure gain.
  • CPU limits earn their place in multi-tenant clusters needing hard isolation from noisy neighbors, and where run-to-run consistency matters (benchmarks). If you set them, watch the throttling metric (container_cpu_cfs_throttled_periods_total) alongside.
  • When throttling shows up, raising or removing the limit comes first; adjusting the app’s thread count comes second.

Memory limits — the opposite: set them by default #

Memory, unlike CPU, is incompressible. The only way to take it back is to kill something, so a leak in a container with no memory limit threatens the whole node, and under node pressure the kernel OOM killer’s choice of victim becomes hard to predict. With a memory limit, at least the leaking container itself dies, with a clear OOMKilled verdict. The setting doesn’t prevent the incident — it makes the incident diagnosable.

  • Set the value at p95-to-peak plus margin (say 120–150% of peak), and when OOMKilled appears, first separate leak from legitimate growth. How free inside a container shows host-wide numbers, and how to tell a cgroup OOM kill apart, were covered in the memory post.
  • Setting requests equal to limits (Guaranteed) buys predictability for critical workloads — because of the eviction order below.

QoS classes — who gets evicted first under pressure #

The combination of requests and limits sets a Pod’s QoS class, which shapes eviction order when node memory runs short.

ClassConditionUnder pressure
Guaranteedrequests = limits on all containersProtected longest
Burstablerequests < limits (or partially set)Middle — worse the more it exceeds requests
BestEffortneither setEvicted first

The rule is simple: things that must not die (databases, core APIs) run Guaranteed, normal services run Burstable, and BestEffort doesn’t belong in production. BestEffort arises from not setting things, so enforcing namespace defaults with a LimitRange is the safe way to prevent it appearing by accident.

The operating routine — this is never set once #

Usage patterns shift with every release, so sizing needs a routine.

  1. A re-baselining cadence (quarterly, or after major releases): watch three things — usage vs. requests, throttling, OOMKilled. The observability stack from K8s Advanced #5 covers the dashboards.
  2. Watch both failure modes as metrics. Over-reservation (high cluster reservation, low usage) and under-provisioning (throttling, OOMKilled) are the same problem in opposite directions.
  3. Remember the HPA interaction. As the HPA post showed, the target percentage’s denominator is requests — change requests and scaling behavior changes with it. Review the two together, always.

Summary #

  • Requests start from measurement (CPU: mean–p95; memory: p95–peak), with VPA recommendation mode as the sizing tool. Oversized requests are an invisible fixed cost.
  • Choose CPU limits knowing the throttling cost. With proper requests, omitting them is a rational default; if set, watch the throttling metric.
  • Memory limits go on by default — not to prevent the incident, but to make it arrive as a diagnosable OOMKilled.
  • Critical workloads run Guaranteed, normal ones Burstable, and LimitRange keeps BestEffort out at the source.
  • Sizing is a routine, not an event: reservation rate, throttling, OOMKilled, re-baselined quarterly.
X