Rightsizing: requests decide your bill, and almost nobody sets them from data.
Most clusters run at a fraction of what they reserve. The gap is not waste in the usual sense — it is requests copied from a template years ago and never revisited. This is how to size them from observed usage without trading cost for reliability.
Requests decide capacity. Limits only cap it.
The distinction is the whole subject, and it is where most rightsizing effort goes wrong.
A deployment requesting 2 cores and using 0.1 removes 1.9 cores from schedulable capacity on that node, permanently, per replica. Multiply by replica count and the reason a cluster needs more nodes than its actual load justifies becomes obvious.
This is also why tuning limits does not reduce cost. Lowering a limit from 4Gi to 2Gi changes nothing on the bill if the request stayed at 2Gi — and it may introduce OOM kills.
Measure before you change anything
# current usage kubectl top pod -n <namespace> --containers # what is actually requested kubectl get pods -n <namespace> -o custom-columns=\ "POD:.metadata.name,CPU_REQ:.spec.containers[*].resources.requests.cpu,MEM_REQ:.spec.containers[*].resources.requests.memory" # how much of the node is reserved versus used kubectl describe node <node> | grep -A6 "Allocated resources"
That last command is the one worth running first. It prints requested percentages per node, and on a typical cluster CPU requests sit at 70–90% while real utilisation is under 10%. That difference is your entire opportunity, and it is visible in one command.
kubectl top shows a single instant, which is not enough to size anything. You need a window long enough to include a real peak — at least a week for a service with weekday traffic, and longer if month-end matters.
Sizing CPU: use P95, not the maximum
CPU is compressible. A container that wants more than its share waits a few milliseconds; it does not die. That makes CPU safe to size close to typical usage.
Set the request near the P95 of observed usage. The workload gets a guaranteed share covering almost all of its real demand, and the rare spike above P95 borrows idle capacity from the node rather than failing.
resources:
requests:
cpu: "150m" # near observed P95
limits:
memory: "512Mi" # memory limit yes, CPU limit usually notSizing CPU requests to the observed maximum is the common mistake. A workload that spikes to 2 cores once a day and sits at 0.1 the rest of the time does not need 2 cores reserved for 24 hours.
Sizing memory: use the peak, because memory is not compressible
Memory behaves in the opposite way. A container that exceeds its memory limit is killed immediately — there is no equivalent of waiting for a free slice. So memory must be sized to the peak, not to a percentile.
Set the request near steady-state usage so scheduling is accurate, and the limit above the observed peak with 25–50% headroom. Sizing memory from an average is how a rightsizing exercise turns into an OOMKilled incident.
resources:
requests:
memory: "256Mi" # steady state
limits:
memory: "512Mi" # observed peak + headroomOne rule worth enforcing mechanically: never propose a memory value below an observed peak, and never reduce memory for a workload that has been OOMKilled recently. Both are signs that the observation window missed the real demand.
Should you set a CPU limit at all?
Often not, and this is genuinely contested. The argument against: a CPU limit throttles the container even when the node has idle cores. The workload gets slower for no reliability benefit, and CFS throttling can add latency in bursts that is hard to attribute later.
The argument for: limits prevent one noisy workload monopolising a node, and on shared clusters that predictability can matter more than peak throughput.
A reasonable default is a CPU request with no CPU limit for latency-sensitive services, and CPU limits on batch or untrusted workloads. Whichever you choose, check whether throttling is already happening before assuming limits are harmless:
# container_cpu_cfs_throttled_seconds_total rising means the limit is biting kubectl top pod -n <namespace> --containers
Memory limits are different — those you should almost always set, because an unlimited container can take a node down.
What you should not rightsize
Rightsizing is only as good as the observation behind it. Some workloads cannot produce that evidence, and a recommendation for them is a guess with a number attached.
The discipline that matters: a recommendation should carry the evidence it was derived from — observation period, sample count, observed peak — so a reviewer can judge it. A number with no provenance is not actionable, and applying one across a fleet is how rightsizing earns its bad reputation.
Evidence beats averages
Two workloads can show the same mean CPU and need entirely different requests. One holds steady at 0.4 cores; the other idles at 0.05 and spikes to 3 during a nightly import. An average describes neither.
Before applying any change, be able to answer four questions: over what period was this observed, how many samples, what was the peak, and did the workload actually run for the whole window. If any answer is missing, the recommendation is not ready.
This is also the honest reason many rightsizing tools are distrusted — they present a confident number without the observation behind it, someone applies it, a workload gets OOMKilled, and the whole practice gets abandoned for a year.
Rolling it out without an incident
kubectl rollout status deployment/<name> -n <namespace> # no new restarts, and usage below the new request kubectl get pods -n <namespace> -w kubectl top pod -n <namespace> --containers
Apply changes through the same GitOps path as any other manifest edit. Rightsizing by kubectl edit is reverted on the next sync and leaves nobody able to explain why the numbers moved.
Frequently asked
Do requests or limits cost money? Requests. They reserve capacity on a node whether used or not.
What percentile should I use? P95 for CPU, observed peak for memory. CPU is compressible and memory is not.
Should I set CPU limits? Often no for latency-sensitive services — they throttle without improving reliability. Memory limits, yes.
Is the Vertical Pod Autoscaler a substitute? VPA automates the same measurement, but it restarts pods to apply changes and needs the same care about workloads whose peak it has not seen.
How much is typically recoverable? Clusters that have never been rightsized commonly reserve several times their real usage. Check Allocated resources on a node against kubectl top for your own number rather than trusting an industry average.
Doing this with evidence attached
The hard part is not the arithmetic, it is having trustworthy observation behind every number and knowing which workloads to leave alone.
KrevoPilot tracks observed usage per workload and proposes requests and limits with the evidence attached — observation window, sample count, observed peak against configured request. It withholds a recommendation where the evidence does not support one: workloads still being observed, jobs, and anything that has not actually run. It will not propose a memory value below an observed peak, and it will not reduce memory for a workload that was recently OOMKilled. The in-cluster agent is read-only, so every change stays yours to apply.
