Cut Kubernetes costs by right-sizing pod requests

Your Kubernetes bill is mostly a node bill, and your node count is mostly a guess. Every pod asks for an amount of CPU and memory up front, called its request. A request is a value in the pod manifest that tells the scheduler how much CPU and memory to reserve for that pod on a node, regardless of what the pod uses at runtime. Set requests too high and you pay for nodes full of reserved capacity that sits idle. Picture a cluster that reserves 75% of its CPU but uses 25%. You are paying for about three times the compute your pods actually run. Here is how to find that gap and close it.

Why requests drive your node count

A request is a reservation, and the scheduler treats it as gospel. A pod lands on a node only if its CPU and memory requests fit inside what that node has left. The sum of every pod's requests is how full Kubernetes believes a node is, regardless of what the CPU is doing this second.

The Cluster Autoscaler reads the same number. It adds a node when a pod cannot be scheduled anywhere because its requests do not fit. It marks a node for removal when the sum of requests on that node falls below a utilization threshold, which defaults to 50% of the node's allocatable capacity. Both decisions count requests. Live CPU and memory usage never enters the math.

So a fleet of pods that each request 1 vCPU but use 0.2 vCPU fills nodes five times faster than the work demands. The autoscaler keeps every one of those nodes, because by request math they look busy, even while kubectl top shows them nearly idle.

Put a number on it. As of 2026, a 4 vCPU, 16 GiB on-demand node runs roughly $100 to $140 a month across the major clouds. If over-requesting pins ten extra nodes, that is $1,000 to $1,400 every month for capacity nothing uses.

What pod rightsizing does to your pricing

Node pricing is the only pricing that moves here. Kubernetes bills you for whole nodes, so your saving equals the price of the nodes you switch off. That makes the payoff easy to size before you touch a manifest.

Add up the slack across your pods, the CPU each one reserves above what it uses. Divide that total by a node's allocatable CPU to see how many nodes the slack pins in place. Multiply by the node's monthly price.

Say your pods over-reserve 40 vCPU between them. On a 4 vCPU node with about 3.8 vCPU allocatable, that slack holds roughly ten nodes hostage. At $120 a month per node, closing the gap puts about $1,200 a month back on the table, once the freed capacity consolidates and the empty nodes leave.

Run the same sum on memory. Price the gap on whichever resource packs your nodes first, because that is the one setting your node count.

The trap: lower requests do not lower the bill until a node leaves

Here is the part most write-ups skip. Cutting one pod's request frees room on its node, but your invoice does not move. The bill only drops when an entire node empties and the autoscaler removes it. And the autoscaler removes a node only when it can safely drain every pod off it first.

One un-evictable pod pins the whole node and its monthly cost. The usual culprits:

Right-sizing requests is the necessary first move. The payoff arrives only when the freed capacity consolidates onto fewer nodes and the emptied nodes can actually leave. Trim the requests, confirm the leftover pods are evictable, then watch a node drop out.

Find your over-provisioned pods by hand

The quickest way to spot an over-provisioned pod is to put two numbers side by side: what it reserves, and what it actually uses. The gap between them is the capacity you pay for and never touch.

The reserved side comes from kubectl describe node. Its output ends with an Allocated resources block that shows CPU and memory requests as a percentage of the node's allocatable capacity.

kubectl describe node ip-10-0-1-23 | sed -n '/Allocated resources/,/Events/p'

The used side comes from metrics-server, which kubectl top reads for live consumption.

kubectl top nodes
kubectl top pods --all-namespaces --sort-by=cpu

When a node reports 78% CPU requested but kubectl top nodes shows 22% actual usage, that 56-point gap is reserved capacity you pay for and never touch. Sort the pod list by CPU and memory to find the workloads carrying the biggest reservations.

To get a per-workload target without guessing, run the Vertical Pod Autoscaler in recommendation-only mode. Set updateMode: "Off" so it watches and recommends but never changes a running pod.

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-rightsizer
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  updatePolicy:
    updateMode: "Off"

Give it at least a day of traffic that includes your normal peak before you trust the numbers, then read the recommendation it stored on the object.

kubectl get vpa api-rightsizer \
  -o jsonpath='{range .status.recommendation.containerRecommendations[*]}{.containerName}{"\n  lower:  "}{.lowerBound}{"\n  target: "}{.target}{"\n  upper:  "}{.upperBound}{"\n"}{end}'

Each recommendation carries a lower bound, a target, and an upper bound. Size CPU requests to the target. Keep the memory upper bound in view, because memory is the resource that bites back if you cut it too close.

Right-size with no downtime

Editing a Deployment's requests triggers a normal rolling update. New pods come up with the smaller reservation while the old pods keep serving, so traffic never drops.

resources:
  requests:
    cpu: 200m
    memory: 256Mi
  limits:
    cpu: "1"
    memory: 256Mi

Apply it and Kubernetes rolls the change one pod at a time.

kubectl apply -f api-deployment.yaml
kubectl rollout status deployment/api

Two safety rails matter more than the savings.

First, memory is uncompressible. A pod that exceeds its memory limit is OOMKilled on the spot, while a pod that exceeds its CPU limit is only throttled and keeps running. So you can trim CPU requests hard, but size memory to the VPA upper bound rather than the average. Starving memory swaps a cost saving for a crash loop.

Second, watch the Quality of Service class. Kubernetes puts every pod in one of three QoS classes (Guaranteed, Burstable, or BestEffort) based on how its requests and limits line up, and it uses that class to decide eviction order when a node runs short. A pod whose requests equal its limits for both CPU and memory earns the Guaranteed class, which is the last to be evicted under that pressure. Drop any request below its limit and the pod becomes Burstable, which is first in line. So for a latency-sensitive service that needs the Guaranteed class, set both CPU and memory requests equal to their limits on every container; matching memory alone is not enough and leaves the pod Burstable. That means giving up the CPU savings on those specific pods, which is the trade you make for eviction protection.

Rollback lives in your manifest. Revert the requests in version control and re-apply, or undo the rollout directly.

kubectl rollout undo deployment/api

A quick decision tree

The choice comes down to three checks in order: is the request well above real usage, can the freed capacity consolidate enough to empty a node, and is the pod latency-sensitive enough to need the Guaranteed class.

graph TD
  A[Pod requests far above its real usage] --> B{Lower the request}
  B --> C{After consolidation, does a node empty?}
  C -- No --> D[An un-evictable pod pins the node. Check safe-to-evict, local storage, and PDBs]
  C -- Yes --> E{Is the pod latency-sensitive?}
  E -- Yes --> F[Set both CPU and memory requests equal to their limits so it stays Guaranteed]
  E -- No --> G[Set CPU to the VPA target, memory to the VPA upper bound]
  F --> H[Node drains and leaves, bill drops]
  G --> H

How OhChimp approaches this

Doing this by hand works. The catch is that the gap hides in hundreds of pods across dozens of namespaces, and the requests that waste the most are buried in workloads nobody owns anymore.

OhChimp reads your cluster's requests against real usage, finds the pods reserving far more than they touch, and writes a reviewable right-sizing plan with the new requests, a confidence score, a risk level, and rollback steps. It checks whether the freed capacity can consolidate onto fewer nodes, so the plan reflects dollars that will actually leave your bill instead of slack on paper. You read it and click apply. Nothing changes until you do, and the saving is then measured against your real bill.

See how the connection works on the Kubernetes integration page.