Right-Sizing Your Pods — A Practical Guide to Kubernetes Resource Requests & Limits

Ever woken up to a pager alert at 3 AM because your application pods were constantly crashing with mysterious OOMKilled errors? Or perhaps your app runs fine on Monday but inexplicably slows to a crawl on Friday afternoon when traffic spikes. This often stems from a single, critical oversight: improperly configured, or entirely missing, resource requests and limits.
In this post, we’ll stop the guesswork and demystify how Kubernetes manages compute resources under the hood. We’ll explore the real-time implications of these configurations and why blindly copy-pasting resource blocks from Stack Overflow is a recipe for disaster.
What You’ll Learn
- The critical difference between CPU (compressible) and Memory (non-compressible) resources.
- How the Kubernetes scheduler uses requests vs. limits.
- The three Quality of Service (QoS) classes and how they affect your pod’s survival during a crisis.
- A line-by-line walkthrough of a real
deployment.yamlresource configuration. - Best practices for setting values based on real-world metrics.
The Core Concept: Compressible vs. Non-Compressible Resources
Think of a Pod as an apartment, and the containers as the specific rooms inside it. The Kubernetes Node itself is the entire apartment building. Naturally, the building only has so much electricity (which we can think of as CPU) and so much physical floor space (which we can think of as Memory).
The building manager (the Kubernetes scheduler) relies on Requests to do its job. A request is the minimum amount of resources guaranteed to your container. It’s used exclusively for scheduling—it’s how the scheduler determines which apartment building in the cluster has enough available, unreserved space for a new tenant.
Limits, on the other hand, are the hard ceilings enforced at runtime via Linux control groups (cgroups). But what happens when your application tries to consume more than its allowed limit? The outcome depends entirely on the type of resource in question.
CPU is Compressible. If your container exceeds its CPU limit, it doesn’t crash. Instead, it gets throttled. The Linux kernel simply restricts its CPU cycles, slowing down your application’s execution speed. It’s exactly like turning down the voltage on your lights using a dimmer switch; everything runs a bit dimmer and slower, but the lights stay on and the app keeps serving requests, albeit with higher latency.
Memory is Non-Compressible.
Memory is ruthless. You cannot compress memory on the fly. If your container tries to allocate more RAM than its limit allows, the node’s Out-Of-Memory (OOM) killer steps in and terminates the process immediately. It’s like trying to fit a king-size bed into a tiny room—it simply won’t fit, and the attempt is blocked completely, resulting in an OOMKilled event.
Quality of Service (QoS) Classes
Kubernetes assigns every Pod a Quality of Service (QoS) class based entirely on how you configure these requests and limits. When a node experiences memory pressure, the kubelet uses these classes to decide which Pods to evict first to save the node.
- Guaranteed: You set both limits and requests, and they are exactly equal. These are the VIP tenants. They are the absolute last to be evicted during a resource shortage.
- Burstable: You set requests lower than limits, or only configure requests. These pods get baseline guarantees but can burst if node resources are available. They are evicted if the node runs out of memory and there are no BestEffort pods left.
- BestEffort: You didn’t configure any requests or limits whatsoever. These are the very first to get evicted during resource starvation.
Implementation Deep Dive: Configuring Our API
Let’s look at how this theory is applied in practice. Here’s what our deployment.yaml resource block looks like for our Node.js Express API:
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
Let’s break down exactly why we chose these specific numbers for our application:
-
WHY
100mfor CPU request? The value100mmeans 100 millicores, which is exactly 10% of a single CPU core. This is a modest, sensible baseline for a Node.js Express API. Node.js is inherently single-threaded and, in most web API scenarios, is mostly I/O-bound (waiting on network requests or database queries) rather than CPU-bound. 10% of a core ensures it has enough baseline compute to idle and handle typical light traffic. -
WHY
500mfor CPU limit? We want to allow the container to burst up to 50% of a CPU core (a 5x burst capability compared to its request). This handles sudden request spikes and CPU-intensive operations quickly, without allowing a runaway process to hog an entire core and starve other critical workloads running on the same node. -
WHY
128Mifor memory request vs512Milimit? Express applications generally have a very low baseline memory footprint when idling or handling steady, light traffic, which is why we request a small128Mi. However, they can spike dramatically in memory usage during heavy JSON parsing, large payload processing, or sudden connection storms. The512Milimit gives the application plenty of headroom to process these large payloads without crossing the threshold that would trigger an immediateOOMKilledtermination. -
WHY are requests != limits here? By configuring limits higher than requests, we assign our Pod the Burstable QoS class. For standard web applications with highly variable, unpredictable load, Burstable QoS is by far the most cost-effective approach. Using Guaranteed QoS (where requests equal limits) would mean allocating 512Mi of memory and 500m of CPU permanently, wasting expensive cluster resources during the many hours when the application is mostly idle.
-
WHY should you never leave resources unset? Leaving resources entirely unset defaults your Pod to the BestEffort QoS class. This means your pods become the absolute first target for eviction when the node faces memory pressure. Furthermore, it introduces the dreaded “noisy neighbor” problem: your unconstrained application could experience a bug, memory leak, or traffic spike, monopolize the node’s resources, and completely starve other critical workloads sharing the same hardware.
The Math: Replicas and Cluster Capacity
When calculating your overall cluster capacity requirements, you must remember that these resource configurations apply per container. If you scale your application horizontally, the math multiplies linearly.
Here’s our replica configuration from the very same deployment.yaml:
spec:
replicas: 2
WHY does the replica count matter for resource planning? Because we have 2 replicas explicitly defined, the total guaranteed memory footprint for this deployment across the cluster isn’t just 128Mi—it’s 256Mi (2 × 128Mi minimum reserved). Your cluster must have this total capacity available just to schedule the pods in the first place. At peak usage, if both of these pods hit their maximum limits simultaneously, they could consume up to 1024Mi of RAM (2 × 512Mi). You have to plan for this peak capacity.
Measuring and Monitoring
How do you actually know what values to set? The golden rule is: never guess. Use real metrics. The simplest way to view real-time resource consumption in your cluster is via the metrics server using kubectl.
# View resources for all pods in the default namespace
kubectl top pods
# Example Output:
# NAME CPU(cores) MEMORY(bytes)
# kubernetes-demo-api-5c7499684c-abcde 25m 85Mi
# kubernetes-demo-api-5c7499684c-fghij 30m 88Mi
In this snapshot, our pods are actively using around 30m of CPU and 88Mi of RAM. This proves that our baseline request of 100m CPU and 128Mi RAM is comfortable and correct for standard operations.
[!NOTE] New in K8s 1.32+: Historically, if you realized your requests or limits were wrong, changing them required restarting the entire pod. Kubernetes 1.32+ has stabilized In-place Pod Resizing. You can now patch a pod’s resources without any disruption, dynamically expanding or shrinking its CPU and memory limits on the fly!
Common Mistakes & Gotchas
[!CAUTION] Leaving Resources Unset: As we discussed, this drops your workload into the BestEffort QoS tier. It is the single most common reason for unexpected pod evictions and degraded cluster performance due to noisy neighbors hogging all node resources.
[!WARNING] Setting Memory Limits Too Tight: Because memory is non-compressible, setting your memory limits too close to your baseline request is dangerous. If you don’t allow room for typical spikes (like garbage collection cycles, caching, or large payload processing), your application will constantly crash with
OOMKilled(Exit Code 137) errors under load. Always give it breathing room.
[!WARNING] Ignoring HPA Max Replicas: When you introduce a Horizontal Pod Autoscaler (HPA) to automatically scale your application, you must calculate your maximum cluster capacity based on the
maxReplicasvalue, not just your starting replicas. If your HPA is allowed to scale up to 10 pods, your cluster nodes must have enough total unreserved capacity to accommodate 10 × your resource requests during a massive traffic storm.
Summary / Key Takeaways
- CPU is compressible: Exceeding CPU limits simply causes throttling and latency.
- Memory is non-compressible: Exceeding memory limits triggers an immediate
OOMKilled(Exit Code 137) termination. - Requests are for scheduling: They reserve guaranteed space on the nodes.
- Limits are for enforcement: They prevent runaway processes from killing the host node.
- Burstable QoS is ideal for web apps: It provides a perfect balance of cost-efficiency and burst capability.
- Always set requests and limits: Avoid the BestEffort trap and noisy neighbor problems entirely.
Series Recap & What’s Next
This brings us to the very end of our 5-part “Zero to Hero with Docker and Kubernetes” series! Let’s take a quick look back at everything we’ve built and learned:
- Containerizing your App: We built our foundational Dockerfile and mastered layer caching.
- Kubernetes Architecture 101: We demystified the Control Plane, Nodes, Pods, and Services.
- Deployments and Services: We deployed our application and made it reliably accessible to the world.
- Liveness and Readiness Probes: We bulletproofed our deployments with intelligent health checks.
- Resource Requests & Limits: We learned to right-size our workloads to prevent OOMKills and optimize costs.
What’s Next? While you now have a rock-solid, production-ready foundation, the cloud-native ecosystem certainly doesn’t stop here. In our next upcoming series, we’ll dive deep into advanced operational patterns. We’ll explore CI/CD with GitOps using tools like ArgoCD, templating our complex manifests with Helm charts, and setting up Horizontal Pod Autoscaling (HPA) to dynamically react to massive traffic storms without breaking a sweat.
Stay tuned, and happy deploying!