Key Takeaways
  • Kubernetes schedules pods based on requests, not limits, which means the request value determines where a pod can run, not just how it behaves once it’s running.
  • Requests and limits together determine a pod’s Quality of Service (QoS) class, and unset values default a workload to BestEffort, the first class evicted under pressure.
  • Setting requests too low causes throttling or OOMKilled (Out Of Memory Killed) errors; setting them too high wastes capacity and blocks the scheduler from placing workloads efficiently.
  • Correct sizing comes from measuring real usage over time, not guessing at deployment, and requires periodic re-checking as workloads change.
  • Zesty’s Multi-Dimensional Autoscaling (MDA) continuously rightsizes pods both vertically (CPU and memory requests) and horizontally (minimum replica counts), with the two dimensions coordinated so they don’t work against each other.

Why Resource Requests and Limits Matter More Than Most Engineers Realize

Kubernetes treats CPU and memory as first-class scheduling resources, and everything downstream of that, which node a pod lands on, how it behaves under pressure, whether it survives a resource crunch, depends on the values set in one field: resources, inside the PodSpec. Set them too low and workloads throttle or get OOMKilled. Set them too high and capacity sits unused, blocking the scheduler from placing other workloads efficiently.

The mechanics behind that tradeoff aren’t complicated once they’re laid out clearly, but they’re also not intuitive from the API alone: Kubernetes schedules pods based on requests, not limits, and that single fact has consequences for scheduling, eviction, and runtime stability that aren’t obvious from reading a YAML file. Most engineers configure these fields once during initial deployment, treat the exercise as done, and move on to the next service, which is a reasonable instinct given everything else competing for attention, but it’s also exactly how a cluster ends up with hundreds of pods sized for a moment in time that’s already passed.

This guide covers what requests and limits actually do, how Kubernetes derives Quality of Service from them, a step-by-step workflow for setting correct values, the misconfigurations that cause the most production incidents, and how to validate that a change actually worked. The mechanics apply whether you’re sizing one service by hand or thinking about how to keep hundreds sized correctly on an ongoing basis, though the second problem, covered toward the end of this guide, is where manual workflows tend to run out of road.

What Requests and Limits Actually Do

Requests and limits are both part of the PodSpec’s resources field, but they control two entirely different things, and conflating them is where most misconfiguration starts.

Requests determine where a pod can run. A request represents the minimum amount of CPU or memory Kubernetes guarantees to a container, and the scheduler uses that value to decide which node can host the pod. A pod won’t be scheduled onto a node unless that node has enough allocatable capacity to satisfy every request the pod is asking for. Set requests too high relative to real usage, and the scheduler reserves capacity nobody uses, artificially reducing how many pods a node can actually hold.

Limits determine how a pod behaves once it’s running. A limit caps the maximum CPU or memory a container is allowed to consume. Exceed the CPU limit and the Linux kernel throttles the container through cgroup enforcement, specifically the Completely Fair Scheduler (CFS) quota, which slows execution without killing anything, but can degrade p99 latency in a way that never shows up in average utilization metrics. Exceed the memory limit and the outcome is more severe: memory can’t be throttled the way CPU can, so the container is OOMKilled and restarted instead.

That asymmetry is worth internalizing on its own: a CPU limit set too tight causes a performance problem, while a memory limit set too tight causes an availability problem. The two failure modes call for different levels of caution when choosing where to set each value.

How Kubernetes Assigns Quality of Service (QoS) Classes

Kubernetes uses the relationship between requests and limits to assign every pod one of three Quality of Service (QoS) classes, and that class determines eviction priority under resource pressure.

QoS ClassHow It’s AssignedEviction Priority
GuaranteedEvery container’s memory and CPU requests equal its limitsStrongest protection, evicted last
BurstableAt least one container has a request, and at least one request doesn’t match its limitEvicted before Guaranteed, protected over BestEffort
BestEffortNo container has any requests or limits set at allEvicted first under memory pressure

The operational risk here is easy to miss: if requests and limits aren’t deliberately set, Kubernetes doesn’t leave the workload in some kind of neutral default. It classifies it as BestEffort by default, the class most likely to be evicted the moment a node comes under memory pressure, and nothing in the deployment process warns anyone that this happened. That’s a common, avoidable cause of unexpected production instability: a workload that was never intentionally deprioritized ends up first in line for eviction simply because nobody set a request value, and the first sign of it is often an incident rather than a warning.

This is worth sitting with for a moment, because it inverts the intuition most engineers bring to Kubernetes. Leaving a field blank usually means “use the default behavior,” which sounds safe. Here, leaving both fields blank means “deprioritize this workload below everything that did set values,” which is the opposite of a safe default for anything that actually matters in production.

A Step-by-Step Workflow for Setting Correct Resource Values

Sizing requests and limits from real usage, rather than guessing at deployment time, follows a repeatable five-step workflow.

Step 1: Observe real usage. Pull actual consumption with a metrics backend such as metrics-server:

kubectl top pod <pod-name> –containers

CPU usage here is averaged over a short window, not instantaneous, so a single reading is a data point, not a verdict. Capture usage over days, not minutes, so normal traffic variation doesn’t get mistaken for the workload’s real ceiling.

Step 2: Compare usage to current requests. Check what’s currently configured against what’s actually being consumed:

kubectl describe pod <pod-name>

Look at the Requests and Limits section for each container. If real usage consistently exceeds requests, the node may be under-provisioned for that pod. If usage sits well below requests, capacity is being reserved and wasted.

Step 3: Choose request values from the data, not a guess. Set CPU requests near typical runtime usage. Set memory requests slightly above the typical high-water mark, since memory doesn’t compress or burst the way CPU does, there’s no equivalent to throttling that buys time before something fails.

A concrete example makes the difference tangible. Say kubectl top pod shows a container typically running at 250m CPU and 380Mi memory over a two-week window, with occasional peaks to 400m CPU and 420Mi memory. A request of 300m CPU and 450Mi memory sits close to typical usage with a reasonable buffer for the peaks, while a request copied from a template at 1 CPU and 1Gi would reserve more than double what the workload actually needs on an ordinary day. That gap, multiplied across every service on a cluster, is exactly where the 40-60% overprovisioning figures commonly cited in industry surveys come from.

Step 4: Decide whether limits are necessary. A safe default pattern: skip CPU limits unless a specific workload needs one, since limits only add throttling risk without much upside for most services, but always set memory limits, since an unbounded container can consume everything on a node and take other workloads down with it.

Step 5: Apply the change and watch it. Roll out the update and recheck usage:

kubectl rollout status deployment/<name>

kubectl top pod

Repeat this cycle until usage lines up with the new requests, then move on to the next workload.

That five-step cycle works well for one service reviewed once. The problem is what happens next: usage shifts with every release, every traffic pattern change, every dependency update, and the values set in step 3 start drifting the moment the review ends. Zesty’s Multi-Dimensional Autoscaling (MDA) runs this same observe-compare-adjust loop continuously instead of as a one-time exercise, re-deriving both CPU and memory requests (vertical) and minimum replica counts (horizontal) from current usage and applying them automatically, with the two dimensions coordinated so they move together rather than fighting over the same signal.

Common Misconfigurations and How to Avoid Them

Five misconfigurations account for most of the resource-related incidents teams actually see in production.

MisconfigurationWhat HappensHow to Avoid It
Requests set far above real usageScheduler reserves unnecessary capacity, reducing effective node densitySize from measured usage, not a guess or a copied template value
Memory limits set too lowContainer hits the limit and is OOMKilled, since memory can’t be throttledSet memory limits above the observed high-water mark with real headroom
CPU limits set too lowKernel throttles the container, degrading latency and throughput even when average usage looks fineSkip CPU limits on latency-sensitive services unless there’s a specific reason to cap them
Requests left unset entirelyPod defaults to BestEffort QoS, the first class evicted under memory pressureAlways set a request value, even a conservative one, rather than leaving it blank
Requests set once and never recheckedValues drift out of sync with usage as traffic, releases, and dependencies changeRecheck on a fixed schedule, or automate the recheck so it doesn’t depend on someone remembering

That last row is the one that undoes the other four over time, and it’s the easiest to miss because nothing about it looks like a mistake in the moment. A team can get requests and limits exactly right during an initial review, ship several releases, and end up drifted well out of alignment with real usage six months later, not because anyone made an error, but because nobody went back to check after the review ended. Zesty’s Multi-Dimensional Autoscaling (MDA) closes that specific gap by treating the recheck as continuous rather than something that has to be remembered and scheduled by a person, adjusting both requests and minimum replica counts together instead of leaving replica tuning to drift on its own.

Validating That Resource Settings Are Actually Working

Three checks confirm whether a resource change actually improved things, rather than just feeling like it should have.

Check 1: Resource usage. kubectl top pod shows whether current consumption tracks reasonably close to the configured requests, not far above or far below them.

Check 2: Restarts and OOMKills. kubectl describe pod surfaces restart counts and OOMKill events. A stable configuration should show neither increasing over time.

Check 3: QoS class. The same kubectl describe pod output shows the QoS Class field directly, confirming a workload landed in the class intended for it rather than defaulting to BestEffort by accident. This check matters even for workloads that seem to be running fine, since a service can look healthy under normal load and still be classified BestEffort, an accident waiting for the first memory-pressure event to expose it.

A workload passing all three checks should show no repeated throttling, no OOM events, and predictable scheduling and performance under normal load. It’s worth running these checks not just immediately after a resource change, but on a recurring basis, since a configuration that passes today can drift out of alignment weeks later as the workload’s real usage shifts underneath it. Checking these three signals once after a change is useful; checking them on an ongoing basis is what actually catches drift before it becomes an incident. Zesty’s approach to compute cost visibility surfaces the request-versus-usage gap continuously at the namespace and workload level, which turns this from a manual post-change check into something that’s always current.

Why Manual Sizing Doesn’t Hold Up Past a Handful of Services

The workflow above holds up well for one team sizing one or two services carefully. It stops holding up once a platform team is responsible for the requests and limits of fifty, or two hundred, workloads at once, and this is worth naming directly rather than leaving as an implied conclusion.

Every deployment shifts a workload’s real resource footprint at least slightly. A dependency upgrade changes memory overhead. A new code path changes CPU behavior under load. Traffic seasonality changes what a normal week looks like. A platform engineer reviewing two or three services closely can catch all of that. Nobody reviews two hundred services with the same attention, on the same schedule, indefinitely, which is exactly why sizing that was correct at rollout becomes wrong within weeks without anyone noticing until a throttling alert or an OOMKill shows up in an incident channel. That’s also why so many clusters carry both overprovisioned services wasting spend and underprovisioned ones failing under normal load, often at the same time, in different corners of the same cluster, because the two failure modes look identical from a distance: a workload nobody’s actively reviewing.

This is also where the usual next steps, Horizontal Pod Autoscaler (HPA) for replica counts, Vertical Pod Autoscaler (VPA) for request tuning, Pod Disruption Budgets for safe eviction, and namespace-level resource quotas, start to show a coordination problem rather than solving the original one. HPA and VPA can both react to the same CPU signal on the same workload, and without coordination, one adjusting requests while the other adjusts replica count, they can produce oscillation instead of stability. Adding more tools to a manual process doesn’t remove the manual process, it just adds more moving parts to keep synchronized by hand.

Zesty’s Multi-Dimensional Autoscaling (MDA) is built specifically for this scale problem: it continuously rightsizes CPU and memory requests (vertical) and optimizes minimum replica counts (horizontal) across every workload in a cluster, with the two dimensions coordinated so they don’t work against each other, delivering more savings than either rightsizing or replica tuning alone would produce.

Conclusion: From a Manual Workflow to a Continuous One

Everything in the workflow above, observing usage, comparing it to requests, choosing new values, deciding on limits, applying the change, and validating it worked, is correct and worth understanding regardless of scale. The limitation isn’t the workflow itself, it’s whether a team has the standing engineering time to repeat it, accurately, across every workload, every time something changes upstream of it.

Zesty’s Multi-Dimensional Autoscaling (MDA) runs that exact workflow continuously instead of on a manual schedule, deriving both CPU/memory requests and minimum replica counts from real-time usage and applying them automatically, with the two dimensions coordinated, so pods stay correctly sized on both axes and correctly classified by QoS without a person re-running the five-step cycle every time a release ships.

Book a Demo with Zesty to see how continuous rightsizing applies to your own cluster’s workloads.

FAQs

What's the difference between Kubernetes requests and limits?

Requests are the minimum resources Kubernetes guarantees a container and what the scheduler uses to decide which node can host a pod. Limits are the maximum a container is allowed to consume before the kernel throttles CPU usage or kills the container for exceeding memory. Requests control where a pod runs; limits control how it behaves once it’s there.

What happens if I don't set requests and limits at all?

Should I always set both CPU and memory limits?

How often should Kubernetes resource requests be re-evaluated?

How does Zesty automate Kubernetes resource sizing?