← Documentation home
Cost and capacity

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.

1RequestReserved on a node by the scheduler, used or not. Unused request is capacity nobody can have. This is what drives node count and therefore cost.
2LimitThe ceiling a container may reach. Exceeding a memory limit gets the container OOMKilled; exceeding a CPU limit gets it throttled. Limits protect neighbours, they do not reserve anything.

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 not

Sizing 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 + headroom

One 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.

1Anything observed brieflyA few hours of data cannot contain a weekly peak. Wait for a full cycle before proposing a reduction.
2Jobs and CronJobsThey run, finish, and disappear. Average usage across a period when they were not running is meaningless.
3Workloads that never ranA pod stuck Pending or crash-looping reports near-zero usage. That is not evidence of low demand, it is absence of evidence.
4Seasonal servicesIf the peak is Black Friday or month-end close and your window is a quiet Tuesday, the data is not representative.

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

1Start with the largest gapsThe handful of workloads reserving many times what they use will deliver most of the saving.
2Change requests firstThey drive cost. Leave memory limits alone in the same change so a regression has one obvious cause.
3One namespace at a timeWatch restarts and latency for a full traffic cycle before continuing.
4Re-measure afterwardsRightsizing is a loop, not a project. Usage moves as the product changes.
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.

Apply for a trial OOMKilled guide Product guide