Engineering
Rightsizing requests with p95, headroom and a floor
Over-sized requests are the most common cause of Kubernetes overspend. Averages break the busiest hour and maximums waste money every other hour. A sizing rule engineers will accept, and what it cannot see.
UnitVane Engineering
September 29, 20266 min read
When a CNCF survey asked why Kubernetes increased their cloud spend, 70% of respondents named overprovisioning (InfoQ, CNCF survey on Kubernetes overspend (March 2024)). Datadog's billing data shows the same thing from the other side: across its customers, 29% of container cost was workload idle, meaning capacity pods requested but did not use (Datadog, State of Cloud Costs 2024).
Requests matter more than usage because of how Kubernetes schedules. A pod is placed on a node only if the node has room for its requests, and that room stays reserved for the pod whether it uses it or not (Kubernetes, Resource management for pods and containers). An inflated request is capacity you pay for and nobody else can use.
Why requests drift up and never come down
- They are copied. A new service starts from the last service's manifest, and its numbers.
- They are set once, under pressure. A launch, an incident or a load test sets a number nobody revisits once things are stable.
- The failure modes are lopsided. A request that is too low can cause throttling or an eviction, which someone notices. A request that is too high costs money every hour, which nobody notices.
- Nobody sees the cost. The bill shows nodes, not requests, so the team that sets the number never sees what it costs. See where Kubernetes spend goes.
Average, maximum or percentile?
Every sizing rule picks a statistic from a window of usage. The choice decides whether the rule is safe, cheap, or neither.
One container's CPU over 7 days, in millicores
- Current request2000m
- Highest hourly sample1400m
- 95th percentile620m
- Average380m
- Proposed request: p95 x 1.2744m
- The average (380m) breaks the busiest hour. Half the samples in a skewed workload sit above it.
- The maximum (1400m) is one sample: a deploy, a cache warm-up, a retry storm. Sizing to it pays for the worst hour every other hour of the week.
- The 95th percentile (620m) ignores the top 5% of samples. With headroom on top, it covers the normal peak without paying for the outlier.
The rule
- Take 7 days of usage per pod, so weekday and weekend patterns are both in the window.
- Use the 95th percentile, or the mean if the mean is higher (it can be, for a workload that is busy most of the time).
- Add 20% headroom.
- Never go below a floor: 0.025 cores for CPU and 0.25 GB for memory, so tiny services keep room to start.
- Flag a container only when usage is under 30% of its request, so small differences don't create churn.
- Report containers using 2% of their request or less as possibly abandoned, not as rightsizing candidates.
cpu = max(p95_cpu x 1.2, 0.025 cores)memory = max(p95_memory x 1.2, 0.25 GB)Memory is not CPU
A container can use spare CPU above its request, up to its limit, and is throttled at the limit; it slows down but keeps running. Memory behaves differently: when a node runs short of memory, pods using more than their request are the first to be evicted, and a container that exceeds its memory limit is killed (Kubernetes, Resource management for pods and containers). Before approving a lower memory request, compare it with the workload's highest memory use, its limit and its restart history.
What an hourly rule cannot see
| Blind spot | Why | What the reviewer checks |
|---|---|---|
| Bursts shorter than the sample interval | A spike between two hourly samples is never recorded | Limits, autoscaler settings, latency targets |
| Monthly and seasonal peaks | A 7-day window doesn't contain month-end batch jobs or a holiday peak | Reject or defer for workloads with peaks outside the window |
| CPU throttling and out-of-memory restarts | Usage shows what ran, not what was held back | Throttling metrics and restart counts |
| How critical the workload is | Cost data doesn't know which service pages someone at night | The owning team decides |
Ship it as a change, not a side effect
A recommendation in a dashboard rarely gets applied. A change with its evidence, a risk level and the patch already written usually does. The patch for the example above is three lines:
resources: requests: cpu: 744m # was 2000m. p95 620m over 7 days, +20% headroom memory: 1.1Gi # was 4Gi. p95 0.9Gi over 7 days, +20% headroomThen shrink the node group. Lower requests move cost from workload idle into cluster idle; the saving reaches the bill only when nodes scale down or pods pack more tightly. Compare the cluster's bill before and after.
Who does what
From infrastructure to an approved change
- 01
Collect
The read-only agent sends cluster snapshots. Billing exports or rate cards provide the cost basis.
Agent + billing
- 02
Allocate
The hub assigns cost to workloads and teams, separates shared and idle spend, and reconciles the totals.
KubeOn hub
- 03
Review
A proposal brings together the saving, evidence, risk and patch. An authorized reviewer records a decision.
Your reviewers
- 04
Apply + verify
Merge the pull request, or let KubeOn apply the change where you granted write access. The saving is then measured.
Your pipeline or KubeOn
Read-only by default. KubeOn changes a cluster only where you grant write access, feature by feature.
Read the workflowWhy 7 days and not 30?
Seven days covers a full weekly cycle while staying recent enough to reflect the current version of the workload. A longer window holds older releases. For workloads with monthly peaks, reject the proposal or size them by hand.
What about limits?
This rule sizes requests, which decide scheduling and cost. Keep limits deliberate: a memory limit protects the node, and a CPU limit can throttle a container that would otherwise use idle CPU.
We run the Vertical Pod Autoscaler. Do we need this?
If VPA manages a workload's requests in auto mode, let it own them, and use proposals for the workloads VPA does not manage. Two systems changing the same request will fight.
How does KubeOn apply this?
KubeOn computes the rule above for every container, prices the saving at your billed rates, and turns it into a proposal with the evidence and the patch. Ship it through your pipeline, or let KubeOn apply it where you granted write access. See the savings methodology.