Cover illustration for “Kubernetes Resource Requests and Limits for Small Teams”

Essay Containers & Clusters

Kubernetes Resource Requests and Limits for Small Teams

Set CPU and memory requests based on measured usage, not copy-paste defaults.

Kubernetes resource requests and limits are not values to copy from the last service that worked. For small teams, getting them right means handling CPU and memory in different ways, measuring what each service needs, and keeping bad defaults out of deployment. That approach prevents avoidable OOMKills, throttling, and wasted spend without requiring a dedicated platform engineer.

Why CPU and memory limits behave differently, and why that gap causes most production pain

A container can keep running even when its CPU limit makes the application painfully slow. When it exhausts its CPU allowance, the Linux CFS scheduler pauses it for the rest of the scheduling period, which is 100ms by default. This pause causes latency, timeouts, and lower throughput. The standard pod status does not show this throttling. A team can end up investigating slow application code when the kernel is imposing the delay.

The useful signal is the ratio of container_cpu_cfs_throttled_periods_total to container_cpu_cfs_periods_total. A ratio above 0.25 causes noticeable latency. Memory has no equivalent slow-down mechanism because it is non-compressible. When a container exceeds its memory limit, the kernel sends SIGKILL, and the pod shows OOMKilled with exit code 137 before restarting from scratch. A pod requesting 256Mi that grows toward 600Mi with a 512Mi limit will be killed even if the node has spare RAM.

The scheduler uses requests to decide where pods fit. Limits take effect after the pod is running. A high request therefore changes how much capacity the scheduler sees as taken, while a high limit changes how far a running container can grow. These are separate decisions, not two versions of the same safety margin.

How the copy-paste anti-pattern silently inflates costs and breaks autoscaling

Generous round numbers often start as a rushed deployment choice and become permanent when someone commits them to a Helm chart. Nothing looks broken, so nobody revisits them. Yet a CPU request far above P95 usage reserves scheduling capacity that the service rarely uses. Across many replicas, that difference can amount to tens of vCPUs. The scheduler treats them as occupied regardless of actual consumption.

That mismatch creates a misleading capacity problem. Nodes look full to the scheduler while their CPUs remain lightly used, and new pods sit pending. The Cluster Autoscaler responds to that scheduling pressure, so inflated requests trigger more nodes. Its default delay before removing underutilized nodes also lets the excess persist. Adding capacity seems to fix the deployment while hiding the error in sizing.

Even requests that look reasonable individually can leave fragmented gaps across nodes that no incoming pod fits. That wastes schedulable space without any one service appearing grossly overprovisioned. Removing resource specifications entirely is not the answer. Pods without requests or limits receive BestEffort QoS and become early eviction candidates under node pressure, while the scheduler lacks useful declarations for placement. Replace guesses with measured requests.

What QoS classes actually decide at runtime, and why Guaranteed matters for production pods

QoS is assigned automatically from requests and limits. It influences which pods are evicted when a node runs short of memory. BestEffort pods go first, other pods face intermediate risk, and Guaranteed pods have the strongest protection. Guaranteed requires every container in the pod to have equal requests and limits for both CPU and memory. Burstable covers pods with resource declarations that do not meet that requirement.

For production workloads that must not be interrupted, Guaranteed protection is valuable. But it comes with a performance tradeoff. Equal CPU requests and limits leave no room to burst beyond the declared CPU allowance without throttling. Setting memory requests below memory limits also prevents a pod from qualifying as Guaranteed, even if the team intended the gap as a precaution.

Equal memory requests and limits alone do not create a separate “Guaranteed for memory” class. QoS belongs to the whole pod. That distinction prevents the sizing framework from promising eviction protection it cannot deliver, and Guaranteed status still does not prevent an OOMKill when a container exceeds its own memory limit.

A concrete sizing framework for teams without a dedicated platform engineer

Start by measuring the service under a load like the one it will face. Use kubectl top pods for a live view, then historical percentiles from container_memory_working_set_bytes and the rate of container_cpu_usage_seconds_total for a less fleeting baseline. PromQL’s rate() needs a range interval. VPA in recommendation-only mode, with updateMode set to "Off", supplies lower-bound, target, and upper-bound suggestions without changing running pods. Idle measurements are not enough to size a service for traffic.

Once you have that baseline, set the CPU request to P95 actual usage plus a buffer of roughly 20%. Then choose the CPU limit by workload. For latency-sensitive APIs, omitting it allows bursts into idle node capacity and avoids a hard cap creating latency spikes. For batch or background work, especially when sharing nodes with interactive services, set the limit at a multiple of the request. Multi-tenant clusters also have a reason to retain caps because one workload’s burst can become another’s noisy neighbor.

Memory needs a stricter decision. Always set a memory limit, since an uncapped container can consume node memory and trigger evictions elsewhere. For production workloads you have measured, use P99 actual memory usage plus a buffer and make the request equal to the limit. Pairing that with a CPU limit above the CPU request is a balanced pattern that leaves burst room while bounding CPU use. It remains in the intermediate QoS class, so workloads requiring Guaranteed QoS must also equalize CPU requests and limits and accept the throttling tradeoff.

Without past data, values based on the runtime are just a place to start. A typical Go or Node.js service can begin with a modest CPU request, a small memory request, and a memory limit several times larger. A JVM service can begin with a moderate CPU request, a mid-range memory request, and a limit roughly double that request. Profiling determines where the values should settle.

With the JVM, heap size accounts for only part of the container memory. Metaspace, thread stacks, direct buffers, and garbage-collection overhead also consume memory. So even when the heap seems to fit, the container can face an OOMKill. The container allocation generally needs considerably more room than the heap alone, rather than a limit copied directly from the heap setting.

The useful signal is the ratio of container_cpu_cfs_throttled_periods_total to container_cpu_cfs_periods_total.

How to stop bad values from reaching production in the first place

A one-time audit cannot stop the next rushed Helm change. Sustainable right-sizing needs enforcement in the same path as the manifests it governs. A small team can introduce the controls incrementally, starting with namespace defaults and moving toward rejection before deployment. Each layer catches a different way that resource declarations go wrong.

LimitRange injects default requests and limits where containers omit them, preventing BestEffort pods from spreading unnoticed through a namespace. It can also set per-container bounds. ResourceQuota then caps total CPU and memory requests across the namespace, turning overprovisioning into a visible budget constraint before it reaches the cloud bill. Defaults prevent omissions, while the quota constrains the aggregate. Neither replaces workload-specific sizing.

Admission policy adds rejection at deployment time for declarations that still violate the rules. Kyverno is an accessible starting point for Kubernetes-focused teams because it uses native Kubernetes YAML, supports default injection, and can generate resources such as LimitRanges for new namespaces. OPA Gatekeeper fits organizations already using Rego across Terraform plans and other configurations.

CI moves the same discipline earlier in the process. Run Kyverno CLI or Conftest against Helm charts and manifests during pull requests so violations appear before the cluster admission controller sees them. A missing resource specification then draws a review comment.

Autoscaling choices that compound correct sizing — and one that silently breaks it

HPA and VPA must not control the same resource metric for the same workload simultaneously. The official Kubernetes VPA documentation identifies this as a known limitation “at this moment.” If HPA scales replicas on CPU, VPA must not manage CPU requests in Auto mode. The controllers fight over the same signal. Correct starting values do not resolve that conflict.

HPA fits stateless services that need their replica count to track CPU or memory load, but its utilization signal needs requests that match actual use. A CPU request that is too high can make a busy deployment look as though it has little to do and stop scale-out. VPA recommendation mode leaves those requests unchanged. For small teams, the practical approach is to leave it "Off" for 14 days, review its suggestions, and apply the changes manually.

Avoiding a restart does not remove the HPA and VPA signal conflict.

KEDA uses signals such as queue depth, request latency, or external metrics when CPU and memory do not describe demand, and supports scaling to zero. Karpenter handles node provisioning by responding to pending pods and sizing nodes to their footprint rather than relying on pre-provisioned fixed node types. Accurate requests help it avoid fragmented capacity. Neither tool makes copied resource values accurate.

Why GPU workloads break CPU-based autoscaling and need a different sizing posture entirely

An LLM inference server can keep every user waiting even as it reports low CPU utilization. Its bottleneck is GPU memory and batch occupancy. A full continuous batch can take up all the server’s capacity without giving HPA the CPU signal it expects. No scale-out follows from a signal that stays low. The workload needs autoscaling based on the resource or demand actually constraining inference.

CPU and host memory sizing still apply. Request the GPU explicitly through nvidia.com/gpu with a value of 1, then size CPU and memory from the inference server’s measured profile. Those requests still decide which node has room for the pod. GPU memory occupancy, however, becomes central to the sizing posture.

The cost of getting this wrong is steep. An underutilized GPU node represents substantial idle spend, so scaling to zero when demand drops is especially useful. Copy-paste overprovisioning also shapes which nodes teams use when they choose the largest GPU instance available by default. The appropriate response is the same measurement discipline applied to a different bottleneck.

How platforms that handle resource management automatically change the calculus for lean teams

VPA recommendations need review and application, LimitRanges and ResourceQuotas need to follow namespace growth, and admission policies need to accommodate new workload types. Each task may be small in isolation. Together, they create a recurring operations commitment that a team without a platform engineer still has to fund with engineering time.

For a modest number of workloads, monthly audits using VPA recommendations and Cluster Autoscaler defaults can remain manageable. As services multiply, copied sizing decisions and policy upkeep make automation the lower-effort path. A team in that position should choose infrastructure that applies resource rules on its own and flags violations before they turn into incidents. Compare platforms by how much recurring management remains.

Resource governance also supports compliance review. Clusters running in the team’s own account, with namespace-level LimitRanges and ResourceQuotas, produce cleaner audit trails for SOC 2 and HIPAA review. For a lean team, the strongest setup is one that keeps those controls dependable without making manual resource management a second job.

Renata Osei-Bonsu

← Back to Cluster Without Ops