Avoid OOMKilled and Silent Throttling: Requests vs Limits for DevOps
Ops guide for DevOps: size Kubernetes requests from p95, set CPU limits 1.5–2×, audit LimitRange and sidecars, and triage throttling vs OOMKilled.
A request reserves capacity for scheduling and is treated as a guarantee once the pod lands on a node. A limit caps what a container can consume at runtime, and the two resource types enforce that cap differently: CPU gets throttled, memory gets a container killed. Namespace policy can quietly rewrite both values before your pod ever starts, and every number is additive across every container in the pod.
TL;DR:
- CPU request should be based on 95th percentile usage, with limits set at 1.5 to 2 times the request to accommodate bursts.
- Memory limits directly cause container termination when exceeded, so sizing must reflect observed steady-state usage plus headroom to avoid
OOMKilled.- Sidecars, telemetry agents, and volume overheads contribute to total pod requests and limits, making pod-level sizing critical for proper scheduling.
- Namespace policies can silently override resource requests and limits via LimitRange and ResourceQuota, which must be reviewed to avoid misconfigurations.
- Use measured data from monitoring tools to set requests accurately and validate with staged rollouts and ongoing metrics analysis before full deployment.
Table of Contents
- What Do Requests and Limits Actually Mean?
- CPU Throttling vs Memory OOM: Two Very Different Failure Modes
- How Pod Totals, Sidecars, and PodLevelResources Change the Math
- How LimitRange and ResourceQuota Reshape Your Pods
- How Do You Actually Pick Good Request and Limit Values?
- What Do Pending Pods, Throttling, and OOMKilled Actually Mean?
- Copy-Ready YAML Patterns for Requests and Limits
- Stability or Utilization: Which Should You Optimize For?
- Get Your Requests and Limits Audited Properly
- Where to Verify This Yourself
- Sources
- FAQ
What Do Requests and Limits Actually Mean?
Every container in a Kubernetes pod can declare two resource fields: resources.requests and resources.limits. Both live under spec.containers[].resources in the pod spec, and both get passed down to the kubelet running on the chosen node. They answer two different questions. The request answers “how much does this container need to run acceptably?” The limit answers “how much is this container allowed to use before something intervenes?”
The Kubernetes documentation is blunt about how the two get used: the scheduler reads requests to decide which node has room for a pod. The kubelet and the container runtime read limits to enforce a ceiling once the container is running. These are separate systems solving separate problems, which is exactly why a badly set pair breaks in two completely different ways.
One quirk trips up a lot of engineers early on: if you set a limit but skip the request, Kubernetes copies the limit value into the request field automatically. Set only a request with no limit, and the container can burst past that number with no ceiling, at least for CPU. For memory, an unset limit means no OOM ceiling either, which is its own risk.
Units matter here, and they are not always intuitive:
- CPU is measured in cores or millicores.
100mmeans 100 millicores, or 0.1 of a CPU core.1000mand1mean the same thing. - Memory is measured in bytes, typically expressed as
Mi(mebibytes, base 1024) rather thanM(megabytes, base 1000). A256Milimit is about 268 million bytes, not 256 million. - CPU is a compressible resource: the kernel can throttle it without killing anything. Memory is not compressible, once it is gone, something has to give.
That distinction between compressible and non compressible resources is the reason the rest of this article splits so cleanly into two enforcement stories.
CPU Throttling vs Memory OOM: Two Very Different Failure Modes
CPU and memory limits get enforced by completely different mechanisms in the kernel, and that difference is the single most misunderstood part of Kubernetes resource management.
CPU enforcement runs through Linux cgroups, which track how much CPU time a container has burned in a given period and throttle it once it hits its limit. The cgroup v2 documentation describes this as a scheduling restriction, not a termination. Your container keeps running. It just gets fewer CPU cycles for the rest of that period, which shows up as slower response times, longer queue depths, and API latency spikes that look like a mystery until someone checks the throttle counters.
Memory works the opposite way. There is no “slow down” mechanism for memory the way there is for CPU. When a container exceeds its memory limit, the kernel’s OOM killer steps in and terminates the process outright. Kubernetes reports this as OOMKilled in the container status, and the pod typically restarts based on its restart policy.
Statistic Callout: Under sustained CPU pressure, a throttled container doesn’t crash. It just gets slower, often invisibly, until someone notices degraded latency and traces it back to a CPU limit set too close to real usage.
The practical difference for your on-call rotation:
- CPU throttling degrades performance silently. Nothing crashes, nothing restarts, and dashboards built around error rates or restart counts will miss it entirely unless you’re watching throttle metrics directly.
- Memory OOM is loud and immediate. The container dies, Kubernetes restarts it, and if the underlying leak or spike persists, you get a
CrashLoopBackOffon top of it. - Both problems trace back to the same root cause more often than not: a limit set from a guess rather than from measured usage.
This is why memory sizing carries a different risk profile than CPU sizing. A CPU limit set too tight costs you latency. A memory limit set too tight costs you the container.
How Pod Totals, Sidecars, and PodLevelResources Change the Math
Kubernetes sums requests and limits across every container in a pod to get the pod-level total used for scheduling and quota accounting. That means your application container is rarely the whole story.
Sidecars, service mesh proxies, and telemetry agents all carry their own requests and limits, and they all count. A pod with an app container requesting 500m CPU and 512Mi memory, plus an Envoy sidecar requesting 100m and 128Mi, plus a logging agent requesting 50m and 64Mi, has a pod-level request of 650m CPU and 704Mi memory. Sizing only around the app container is one of the most common hidden mistakes teams make, and it is exactly the pattern the Google Cloud team calls out when they describe under-requested pods that clear scheduling but choke under real load.
emptyDir volumes add another wrinkle. Without a sizeLimit set, an emptyDir volume can grow to consume space up to the pod’s memory limit if it’s backed by tmpfs, which quietly eats into the same memory budget your containers are competing for.
- Always size the pod, not just the primary container.
- Set
sizeLimiton everyemptyDirvolume that could grow unpredictably. - Check whether your mesh or agent vendor publishes recommended request/limit values, and start there instead of guessing.
Kubernetes v1.34 introduced PodLevelResources as a beta feature, letting you set requests and limits once at the pod level instead of per container. It’s a genuine convenience, but the feature announcement notes real caveats, including no Windows node support yet. Until your cluster version and workload support are both confirmed, container-level fields remain the safer, more portable baseline.
Pro Tip: Run kubectl describe pod on a pod with sidecars and manually add up every container’s requests before you trust your Helm chart’s defaults. Charts frequently size the app container and forget the sidecar entirely.
How LimitRange and ResourceQuota Reshape Your Pods
Namespace policy can silently change the values you actually submitted, and this is where a lot of “why did my pod get rejected” tickets originate.
A LimitRange object lets cluster admins set default and defaultRequest values that get injected into any container that omits them, along with min, max, and maxLimitRequestRatio constraints. The Kubernetes documentation on LimitRange is specific about this: these defaults apply at admission time, meaning the object that actually gets scheduled may not match the YAML you wrote.

ResourceQuota works at a coarser level, tracking aggregate requests.cpu, requests.memory, limits.cpu, and limits.memory across an entire namespace. If a quota exists on a namespace and a new pod would push the total over that ceiling, the ResourceQuota documentation confirms the control plane rejects pod creation outright rather than admitting it and sorting things out later.
Here’s a practical diagnostic sequence when a pod won’t schedule and you suspect namespace policy:
- Run
kubectl describe limitrange -n <namespace>to see what defaults and ranges are active. - Run
kubectl get pod <pod> -o yamland check the admitted resource values, not your original manifest. LimitRange can inject adefaultlimit that ends up lower than a request you explicitly set, which makes the pod unschedulable in a way that looks like a scheduler bug. - Run
kubectl describe resourcequota -n <namespace>to see current usage against the ceiling. - Check the pod’s events with
kubectl describe podfor quota-related rejection messages, which usually spell out exactly which quota dimension was exceeded.
Treat the admitted object as ground truth. The YAML you submitted is a request to the API server, not a guarantee of what actually gets scheduled.
How Do You Actually Pick Good Request and Limit Values?
Start from measured data, not intuition. Pull p95 CPU usage and memory RSS from whatever metrics stack you already run, whether that’s Prometheus, Datadog, or a cloud provider’s monitoring console, and use those percentiles as your request baseline rather than the peak or the average.
A few starting patterns that hold up across most stacks:
- Stateless web services: set CPU request near p95 usage, CPU limit at 1.5 to 2 times the request to absorb bursts, memory request near observed steady state, and memory limit with modest headroom since OOM kills are far more disruptive than throttling.
- Background workers and batch jobs: CPU limits can run tighter since latency matters less, but memory limits need generous headroom if the job processes variable-sized payloads.
- JVM-based applications: memory requests and limits should sit close together, because the JVM heap is a fixed allocation regardless of instantaneous load, and a wide gap between request and limit just invites eviction risk without buying flexibility.
Requests interact directly with autoscaling. The Horizontal Pod Autoscaler scales replica count based on utilization against the request value, so an inflated request understates real utilization and delays scale-out. The Vertical Pod Autoscaler is generally a better fit for right-sizing memory over time, since memory usage tends to be more stable and predictable than CPU, which can spike unpredictably under load. Our guide on right-sizing pods with autoscaling walks through this interaction in more depth.
Pro Tip: Never push a new request/limit pair straight to full production traffic. Roll it through a staging canary first, watch throttle and OOM metrics for at least one full traffic cycle, including your peak hour, then expand gradually while watching the same dashboards.
Rules of thumb only get you the first draft. The validation loop, canary, monitor, expand, is what actually catches a bad guess before it becomes an incident.
What Do Pending Pods, Throttling, and OOMKilled Actually Mean?
Different symptoms point to different root causes, and matching them correctly saves you from chasing the wrong fix at 2 AM.
- Pod stuck Pending. Run
kubectl describe podand check the Events section for scheduling failures. Then runkubectl get nodes -o jsonand compare allocatable capacity against your pod’s requests. If nodes have room, checkkubectl describe resourcequotafor a namespace-level ceiling blocking admission instead. - Sustained CPU throttling. If Prometheus is running, check
container_cpu_cfs_throttled_periods_totalagainstcontainer_cpu_cfs_periods_totalfor a throttle ratio. A container consistently near or at its CPU limit with rising throttle counts needs either a higher limit or a genuine performance fix, not just more headroom. - OOMKilled containers. Check
kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'for confirmation, then check the node’sdmesgoutput for kernel OOM killer log entries and graph memory RSS over time to see whether it was a spike or a slow leak. Our breakdown of Linux OOM killer behavior covers how to read those kernel logs correctly. - Evictions under node pressure. Check the pod’s QoS class with
kubectl get pod -o jsonpath='{.status.qosClass}'. The Kubernetes eviction documentation confirms BestEffort pods get evicted first under memory pressure, Burstable pods are next, and Guaranteed pods, where requests equal limits, are protected longest.
Statistic Callout: A pod’s eviction order under node pressure comes down almost entirely to QoS class. Guaranteed pods, where every container’s request equals its limit, sit at the bottom of the eviction priority list; BestEffort pods with no requests at all sit at the top.
If evictions keep recurring on the same node, check for disk pressure and image garbage collection too. Not every eviction is memory-related, and our post on kubelet disk pressure and image GC covers the other common trigger.
Copy-Ready YAML Patterns for Requests and Limits
A few short, annotated examples cover most real-world configurations you’ll actually write.
Request only (no ceiling, can burst on CPU, no memory OOM ceiling either, which is risky):
resources:
requests:
cpu: "250m"
memory: "256Mi"
Limit only (Kubernetes copies this value into the request automatically):
resources:
limits:
cpu: "500m"
memory: "512Mi"
Request and limit together (the pattern you want for anything production-facing):
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
Two-container pod with a sidecar, showing why pod totals matter:
- App container: requests
300mCPU,256Mimemory - Sidecar: requests
100mCPU,128Mimemory - Pod total for scheduling: all container requests summed for CPU and memory
An emptyDir with an explicit ceiling avoids the unbounded memory-backed volume risk covered earlier:
volumes:
- name: cache-volume
emptyDir:
sizeLimit: "500Mi"
Remember the unit shorthand: 100m is 100 millicores, one tenth of a CPU core, and Mi always means mebibytes (1024-based), not the decimal megabyte your monitoring dashboard might label as M.
Stability or Utilization: Which Should You Optimize For?
Conservative sizing earns its keep on anything customer-facing or stateful, where an OOM kill means a real incident, not just a wasted node-hour. If a workload has unpredictable memory growth or feeds an SLA, size generously and eat the utilization cost. It’s cheaper than a page at 3 AM.
That logic breaks down for internal batch jobs, dev environments, or anything that tolerates a restart without consequence. There, tight requests and aggressive bin-packing save real money, and the failure mode is a slow job, not an outage.
The efficient move is triaging namespaces by blast radius before auditing anything. Start with namespaces running Guaranteed-QoS production workloads, since misconfiguration there costs the most. A structured audit, whether internal or via a service like a Kubernetes Health Check, tends to surface the sidecar-sizing and LimitRange-injection problems this article covers faster than reading dashboards cluster by cluster.
— James
Get Your Requests and Limits Audited Properly
Reading through cgroup throttle metrics and LimitRange defaults across a dozen namespaces is exactly the kind of work that eats a week when you do it manually and still misses something. A Kubernetes Health Check is a one-off engagement built around exactly this problem: a resource sizing review across your workloads, a full audit of your LimitRange and ResourceQuota objects, and an analysis of your OOM and eviction history to find where namespace policy is quietly working against you.

You get a prioritized remediation plan out of it, not a slide deck of generic advice, ranked by which fixes reduce incident risk fastest. If you’d rather browse what else is on offer first, from the OpenStack Operations Toolkit to observability reviews, the full pricing page lays out plans and services. Book the health check directly through work with me and get your cluster’s actual resource picture instead of another round of guesswork.
Where to Verify This Yourself
The Kubernetes project’s own documentation is the definitive source on every mechanism covered here: resource management for pods and containers for requests and limits fundamentals, LimitRange and ResourceQuota for namespace policy, and node pressure eviction for QoS-driven eviction ordering. The PodLevelResources feature announcement covers the newest beta capability and its current limitations directly from the Kubernetes blog.
FAQ
What Is the Difference Between Requests and Limits in Kubernetes?
A request is what the scheduler reserves on a node before your pod ever runs; a limit is the runtime ceiling enforced by the kubelet and kernel once it’s running. Exceed a CPU limit and you get throttled; exceed a memory limit and the container gets killed.
What Does “CPU 100m” Mean in a Kubernetes Request?
100m means 100 millicores, or one tenth of a single CPU core. 1000m is equivalent to 1 full core.
What Happens if a Pod Exceeds Its Memory Limit?
The kernel’s OOM killer terminates the container, Kubernetes marks it OOMKilled, and the container restarts according to the pod’s restart policy. Unlike CPU, there’s no throttling option for memory since it isn’t a compressible resource.
Is Kubernetes Still Relevant in 2026?
Yes. It remains the dominant orchestration platform for containerized production workloads, and features like the PodLevelResources beta introduced in Kubernetes v1.34 show active development specifically around resource management, the exact area this article covers.
How Do I Choose Starting Values for Requests and Limits?
Pull p95 CPU and memory usage from your existing metrics and use those as your request baseline, then set limits with headroom scaled to your workload type. A staged rollout through a canary, with throttle and OOM metrics monitored before wider deployment, catches bad guesses before they hit full production traffic.
Recommended
- When the Cloud Throttles You: Diagnosing Quota and
- Right-Sizing Pods: Resource Requests, Limits, and Autoscaling That Works
- Taming the Linux OOM Killer: Tuning Out-of-Memory Behavior
- AWS Error Guide: ‘Throttling: Rate exceeded’ and
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.