Kubernetes Resource Requests and Limits: Best Practices
DevZero Team

Kubernetes resource requests and limits are the two numbers that decide whether your pods run reliably or get throttled and killed. A request is what a pod is guaranteed; a limit is the ceiling it can never cross. Get the gap between them wrong, and you either waste money on idle capacity or get paged at 2 a.m. for an OOM-killed pod.
Most teams get it wrong in both directions at once, and it typically costs them 30 to 60 percent of their compute bill.
If you've been asked to "just set some limits" on a cluster that's grown past the point where anyone remembers why the current numbers exist, this blog is for you.
What Are Kubernetes Resource Requests and Limits?#
A request tells the Kubernetes scheduler how much CPU and memory a container needs to run. A limit tells the kubelet the maximum it's allowed to consume. The scheduler uses requests to decide which node has room for a pod; it never looks at limits when placing pods.
That distinction trips up more engineers than anything else in this topic. People assume the limit is what gets scheduled against. It isn't.
A node can happily accept ten pods whose limits, added together, exceed its total capacity, because Kubernetes only checks requests at scheduling time.
This is called overcommitment, and it's deliberate. It's also the reason clusters that look "full" on a dashboard often have plenty of headroom, and why clusters that look "fine" can still evict pods without warning.
Here's the spec shape, since it comes up in every code review:
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"CPU is measured in millicores, where 1000m equals one vCPU. Memory is measured in bytes, usually written as Mi or Gi. Both are set per container, and Kubernetes adds them up across every container in a pod to get the pod's total footprint.
What Actually Happens When a Pod Hits Its Limit?#
CPU throttles. Memory kills the container. That asymmetry is the single most important thing to understand before you write another resource block.
CPU is a compressible resource. When a container hits its CPU limit, the Linux kernel's CFS (Completely Fair Scheduler) throttles it, meaning it gets less CPU time in each scheduling window. The process doesn't die. It just gets slower, sometimes dramatically slower, and this shows up as latency spikes that are maddening to debug because nothing in the logs says "throttled." You have to go looking for it in container_cpu_cfs_throttled_periods_total metrics.
Memory is not compressible. There's no such thing as "slightly less memory, but slower." When a container tries to allocate more memory than its limit allows, the Linux OOM killer terminates it immediately. Kubernetes reports this as OOMKilled with exit code 137.
If the pod is managed by a Deployment, StatefulSet, or DaemonSet, the controller restarts it right away, which is why you'll sometimes see a pod bounce back before anyone notices, and other times see a crash loop that pages the whole team.
This is why the standard advice, repeated in almost everywhere on this topic, is to set memory requests close to limits (or equal) and give CPU more room to breathe. A CPU spike costs you latency for a few seconds. A memory spike costs you a killed pod, a restart, and possibly a dropped request in flight.
What Are Kubernetes QoS Classes, and Why Do They Matter More Than People Think?#
Kubernetes assigns every pod a Quality of Service class based on how its requests and limits compare, and that class decides who gets evicted first when a node runs out of memory.
There are three classes:
Guaranteed#
Applies when every container in the pod has CPU and memory limits set, and the request equals the limit for both. These pods are the last to be evicted under memory pressure. They're also the safest to run stateful, latency-sensitive workloads on.
Burstable#
Applies when at least one container has a request set, but requests don't equal limits. This is the most common class in real clusters, because it's what you get by default when you follow the "set a request, set a slightly higher limit" pattern. Burstable pods get evicted after BestEffort pods but before Guaranteed ones.
BestEffort#
Applies when no requests or limits are set at all. These pods get scheduled wherever there's room and are the first to be killed when a node is under memory pressure.
Most teams don't choose their QoS class on purpose. It falls out of whatever resource block got copy-pasted from a previous service. That's a mistake worth fixing deliberately: put your genuinely critical, stateful workloads on Guaranteed, keep most stateless services Burstable, and treat BestEffort as something you only accept for genuinely disposable batch jobs, not because it slipped through review.
Why Do Kubernetes Pods Get OOMKilled Even When Usage Looks Fine on a Dashboard?#
Because dashboards usually show average usage, and OOMKilled events happen on spikes, not averages. A pod that sits at 40 percent of its memory limit for 23 hours a day can still get killed by a five-second spike during a batch import, a garbage collection pause, or a traffic burst that a 15-minute Grafana panel will smooth right over.
This is the gap between what engineers see and what actually happens on the node, and it's one of the most common complaints in Kubernetes forums and Stack Overflow threads about resource limits: people set limits based on "normal" usage, get paged for OOMKills during traffic spikes, and then overcorrect by doubling every limit across the board, which quietly reintroduces the overprovisioning problem they were trying to avoid.
The fix isn't "set limits higher." It's setting limits based on peak usage percentiles (p95 or p99) over a long enough window to catch your actual spike behavior, not just the last hour.
That requires historical data most teams don't have sitting in a dashboard, which is exactly why manual right-sizing tends to stall out after the first pass.
How Do You Actually Calculate the Right Resource Requests for a Kubernetes Workload?#
Start from real usage data and give yourself a buffer sized to your workload's actual variance, not a flat rule of thumb.
The practical approach: pull CPU and memory usage for each workload over at least two to four weeks, covering your peak traffic days and any batch jobs.
- Set memory requests at roughly your p95 to p99 usage, since memory spikes are the ones that kill pods.
- Set memory limits equal to or slightly above the request, because there's little upside to giving memory a wide gap; you're just delaying the inevitable OOMKill instead of preventing it.
For CPU, you can afford more slack between request and limit, since throttling degrades performance instead of killing the container.
A common pattern is setting the CPU request at your median or p75 usage and letting the limit sit two to three times higher, or removing the CPU limit entirely and relying on requests plus namespace-level quotas to keep things fair.
kubectl top pods gives you a live snapshot, but it's a snapshot, not a trend, so it won't catch spikes that happen outside the moment you run the command.
For real right-sizing, you need something ingesting metrics continuously, which is where most teams reach for the Kubernetes metrics-server plus Prometheus, or a platform built specifically to do this analysis without you having to build the pipeline yourself.
This is the exact problem DevZero's platform was built to solve. It profiles every workload continuously, using real usage history rather than a point-in-time snapshot, and recommends (or automatically applies, with guardrails) requests and limits based on actual behavior instead of a rule of thumb an engineer typed in eighteen months ago.
If you want to see what your own cluster's requests look like against its actual usage, DevZero's read-only assessment installs in under 45 seconds and doesn't touch anything until you approve a change.
Why Are Most Kubernetes Clusters So Overprovisioned?#
Because engineers set requests to avoid pages and nobody goes back to tighten them once the fear of an incident is gone.
This is the pattern across almost every Kubernetes cost teardown in the industry: teams set generous requests early to avoid instability, ship it, move on, and the numbers never get revisited because touching resource limits on a running production service feels risky and nobody wants to own that risk.
Our own benchmark work on this problem shows the gap between requested and actually used CPU and memory sitting consistently in the range of 60 to 80 percent across the workloads they analyzed.
The clearest real-world example of this we've seen is Fi Money, an Indian fintech serving over 3 million users, where CPU requests on some workloads were set 88 percent above actual usage, meaning the team was paying for nearly nine times the compute they needed.
Their founding engineer, Prasanna Ranganathan, put it this way: engineers were "manually guessing resource requests with no data-driven guidance," and the team had no cluster-level efficiency metrics to even know where the waste was sitting.
That's not a careless team. Fi is a regulated financial services company where a bad deploy has real consequences. The overprovisioning wasn't laziness; it was a rational response to a system that punishes you far more visibly for under-requesting than over-requesting. An OOMKill during a product launch gets noticed. A cluster running at 20 percent utilization does not, until someone finally reads the AWS bill line by line.
What Does "Right-Sizing" Actually Look Like in Practice?#
It's a continuous loop, not a one-time cleanup project, and the moment teams treat it as a quarterly spreadsheet exercise, it starts drifting again within weeks.
Fi Money's staging cluster is a useful case because the numbers are specific and verified rather than rounded up for a marketing slide.
After DevZero's autoscaling was deployed, staging saw a 47 percent cost reduction, with CPU usage down 41 percent and memory down 42 percent.
The UAT cluster went further: a 67 percent cost reduction, with CPU down 61 percent and memory down 71 percent, all before production was even touched.
Software engineer Parth Agarwal described what changed day to day: "I used to dread deployment days because we never quite knew if a resource issue was going to cause problems in staging that wouldn't show up until prod. DevZero just removed that anxiety entirely. Things behave the way you expect them to."
That quote matters more than the percentage. The real cost of bad resource sizing isn't only the cloud bill; it's the hours engineers spend second-guessing whether a resource constraint, not a code bug, caused an incident. Fi's Senior Engineering Manager Sakthi Natesan framed the shift as moving "from guesswork to evidence-based scaling," freeing engineers to build the banking platform instead of babysitting infrastructure.
The same pattern shows up at Databahn, an AI data infrastructure company running across AWS, Azure, and (soon) OCI and GCP, which cut its AWS cluster costs by roughly 75 percent using the same continuous rightsizing approach.
Head of Architecture Mihir Nair described the compounding value of doing this once and applying it everywhere: "DevZero's consistent approach means we build expertise once and apply it across the board. That's a massive competitive advantage as we scale."
Do You Build Your Own Kubernetes Cost Monitoring, or Buy a Platform for It?#
If you're a startup weighing this, the honest answer depends on how much engineering time you're willing to spend on infrastructure work that isn't your product, and for most teams under 50 engineers, the math doesn't favor building.
Building your own setup typically means wiring together the Kubernetes metrics-server, Prometheus for historical data, Grafana for dashboards, and something like the Vertical Pod Autoscaler (VPA) in recommendation mode to suggest new requests.
Each piece is free and well documented. None of them talk to each other out of the box, and none of them account for cloud pricing, instance types, spot availability, or Savings Plans, which means you still have to build a separate layer just to turn "this pod is oversized" into "and here's what it should cost instead."
Cloud FinOps vendors like Flexera and ScaleOps have both written extensively about this gap between raw Kubernetes metrics and actual cost attribution, because it's the step most homegrown setups never get to.
The VPA itself is worth naming specifically because it comes up in almost every forum thread on this topic.
It's a genuinely useful open-source tool for recommending CPU and memory requests based on historical usage, and in recommendation-only mode it's low risk.
But its "auto" mode requires evicting and recreating pods to apply new resource values, which means downtime unless you're running it alongside something that handles live migration, and it has no concept of node selection, spot pricing, or cross-cloud instance comparison. It solves one piece of a bigger problem.
That's the gap DevZero was built to close, and it's worth being specific about what it actually does rather than treating it as a generic dashboard. It profiles every workload continuously, picks the most cost-effective node for each pod through context-aware binpacking, and adjusts CPU, memory, and GPU provisioning in real time, all through a read-only operator that installs in under 45 seconds with zero upfront configuration.
Because it uses checkpoint-restore for live migration, it can resize workloads without the restart-and-hope approach that makes teams nervous about touching production limits in the first place.
For a startup with a lean infrastructure team, that's the real decision: building the open-source stack buys you full control at the cost of ongoing engineering time that never shows up on a roadmap slide, while a platform like DevZero trades a subscription fee for getting that engineering time back, typically showing measurable savings within the first two weeks of a read-only assessment.
What Should You Actually Look for in a Kubernetes Resource Optimization Tool?#
Five things matter more than the marketing page, and most vendors are quiet about at least one of them.
Does it change resources without restarting your pods?#
A lot of "autoscaling" tools still rely on evicting and recreating pods to apply new limits, which means every optimization comes with a small risk window. Live migration through checkpoint-restore avoids that entirely.
Does it start read-only?#
You should be able to see exactly what a tool would change before it changes anything, especially in regulated environments like fintech or healthcare where "trust us, it's safe" isn't a compliance-acceptable answer. Fi Money specifically chose this path because stability was non-negotiable in a regulated environment.
Does it price against real instance data, not a flat multiplier?#
Node-level savings only work if the tool actually knows what a memory-optimized m6i.8xlarge costs versus a compute-optimized c6i.8xlarge in your specific region, across your specific cloud provider, right now. DevZero tracks over 3,000 instance types and 69,000 price points across AWS, Azure, GCP, OCI, and OpenShift to make that comparison meaningful rather than approximate.
Does it work across every cloud you actually run on, not just the one the vendor built first?#
Multi-cloud is increasingly the default for growth-stage companies, and a tool that only optimizes AWS is a tool you'll outgrow.
Does it give you cost attribution by team, product, or workload, not just a cluster-wide total?#
You can't build a business case for optimization, or hold a team accountable for its own spend, if the only number you have is "the whole cluster costs $X."
What's a Concrete Way an Engineering Team Would Roll This Out?#
Start read-only, validate on the least critical cluster, and expand cluster by cluster once the data proves itself.
Fi Money's rollout is a useful template because it wasn't a big-bang migration. The infrastructure team installed a read-only operator first, which surfaced actionable efficiency data within the first week without touching a single running workload.
Once that data validated the scale of the opportunity, they moved cluster by cluster, starting with staging and UAT before touching production, confirming results at each stage before expanding.
That phased approach is also why the ROI case became "impossible to argue with," in Prasanna Ranganathan's words, by the time production was even on the table.
If you're running this yourself with open-source tooling, the equivalent sequence is: deploy VPA in recommendation mode only, let it collect two to four weeks of data, manually apply the recommended requests to a non-production namespace first, watch for CPU throttling and OOMKilled events in your monitoring, and only then roll changes to production behind a canary or a small percentage of traffic.
Skipping the observation window is the single most common reason teams get burned trying to right-size on their own.
How Do You Know if This Is Worth Fixing Right Now?#
If your cloud bill has grown faster than your traffic, or if your team has ever said "we're not sure why that pod got killed," you already have your answer, and the fastest way to know the size of the opportunity is to measure it rather than guess at it.
The pattern across every case study and benchmark cited here is consistent: teams that never intentionally overprovisioned still end up paying for two to nine times the compute they actually use, because resource requests are set once under pressure and rarely revisited.
That gap doesn't announce itself. It shows up, month after month, on an invoice that keeps climbing while nobody can point to exactly why.
If you want to see the size of that gap on your own infrastructure before committing to anything, DevZero's free assessment runs a read-only scan of your cluster and shows you overprovisioned workloads, idle capacity, and a real savings estimate, usually within minutes and without a single configuration change on your end.
FAQs#
Should you set a CPU limit at all, or just a CPU request?#
A growing number of platform teams skip the CPU limit entirely and only set a request. Here's why: a CPU limit doesn't protect anything the way a memory limit does. It just throttles a pod that has idle capacity sitting right next to it on the same node, which hurts latency for no real safety benefit. Memory limits are non-negotiable, since they prevent one pod from starving every other pod on the node. CPU limits are optional, and many teams find performance improves once they remove them and rely on requests plus namespace quotas to keep things fair.
What happens if you don't set any requests or limits on a pod at all?#
The pod gets the BestEffort quality of service class, and the scheduler places it wherever there's spare room, with no guarantee of resources. That's fine for a genuinely disposable job. It's a problem when it happens by accident, which is common: a container with no resource block will still run and pass CI, so nobody notices until the node comes under memory pressure and BestEffort pods are the first killed, without warning, regardless of how important the workload actually is.
Can a cluster admin force every pod to have requests and limits, instead of relying on developers to remember?#
Yes, using a Kubernetes object called a LimitRange, set at the namespace level. It defines default requests and limits that apply automatically to any container that doesn't specify its own, and it can also enforce minimum and maximum values so nobody accidentally deploys a pod requesting 64 CPU cores. Pairing a LimitRange with a ResourceQuota, which caps total consumption for the whole namespace, is the standard way platform teams stop overprovisioning at the source instead of catching it after the fact in a cost review.
How do resource requests affect your Horizontal Pod Autoscaler?#
HPA scales replica count based on a percentage of the resource request, not the limit and not raw usage. If your CPU request is set too high, HPA thinks the pod has plenty of headroom and won't scale out until usage climbs much further than it should, which shows up as slow scaling during real traffic spikes. If the request is set too low, HPA overreacts to normal fluctuation and adds replicas you don't need. Getting requests right isn't just a cost question. It's what makes your autoscaling behave the way you actually expect it to.
Do GPU resource requests work the same way as CPU and memory requests?#
No, and this trips up teams moving from standard workloads into AI infrastructure. GPUs are requested as whole integers only. There's no such thing as requesting "0.3 of a GPU" through standard Kubernetes scheduling, and there's no concept of a GPU limit separate from the request, since Kubernetes doesn't allow GPU overcommitment the way it allows CPU overcommitment. This is exactly why GPU clusters run so underutilized in practice: a workload that only needs a fraction of a card still reserves the whole thing, and fixing that requires either NVIDIA's MIG partitioning or a platform built to manage fractional GPU allocation directly.
How often should you actually review and update resource requests and limits?#
Continuously, or at minimum every time a workload's traffic pattern changes meaningfully, not on a fixed quarterly schedule. Static review cycles miss the exact thing that causes waste in the first place: a service that shipped a new feature, absorbed a growth spurt, or quietly changed its usage pattern six weeks after the last review. That gap is precisely why manual rightsizing tends to drift back to overprovisioned within a few months of any one-time cleanup, and why teams that see lasting results tend to run this as an automated, always-on process rather than a project with an end date.
DevZero Team

