Cut Kubernetes costs by right-sizing pod requests

A Kubernetes node packed with pods reserving far more CPU and memory than they use, beside a smaller right-sized cluster running the same work

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

Paying for capacity you reserved and never touched is the same bill in a few different shapes.

Common questions

How do I know whether my Kubernetes pods are over-provisioned? Put the reserved number next to the used number. kubectl describe node ends with an Allocated resources block that shows CPU and memory requests as a percentage of the node's allocatable capacity, and kubectl top nodes shows what that same node is actually burning. A node reporting 78% CPU requested against 22% used carries a 56-point gap, and you pay for every point of it. Run kubectl top pods --all-namespaces --sort-by=cpu to see which workloads hold the biggest reservations.

What is the difference between requests and limits in Kubernetes? A request is a reservation. The scheduler uses it to decide which node a pod fits on, and the sum of requests is what your node count is built from. A limit is a runtime ceiling the kubelet enforces once the container is running. Requests decide how many nodes you rent. Limits decide what happens to a greedy container: over its CPU limit it gets throttled and keeps serving, over its memory limit it is OOMKilled on the spot.

Will lowering CPU requests lower my Kubernetes bill? Only once a node empties and leaves. Freeing 200m on a node that keeps running moves nothing on the invoice, because you rent the node rather than the reservation inside it. The Cluster Autoscaler marks a node for removal when the requests on it drop below its utilization threshold, which defaults to 50% of allocatable capacity, and it removes the node only if it can drain every remaining pod first.

Why did my node count stay the same after right-sizing? Something on those nodes refuses to move. Check for the cluster-autoscaler.kubernetes.io/safe-to-evict: "false" annotation, pods holding local storage like emptyDir or hostPath, a PodDisruptionBudget that never permits a voluntary disruption, and bare pods with no owning controller. Any one of them pins the node and its full monthly cost.

Can I change a pod's CPU and memory without restarting it? On a running pod, yes. Kubernetes exposes a resize subresource that changes CPU and memory in place, which kubectl patch pod <name> --subresource resize drives directly on kubectl v1.32.0 and later. Each container's resizePolicy decides whether a given resource needs a restart: CPU defaults to NotRequired, while memory commonly carries RestartContainer. The pod keeps its QoS class across the resize. A Deployment is the other story, because editing its template rolls new pods, which is why the rolling update above is the route for a whole workload.

Can I run the Vertical Pod Autoscaler and the Horizontal Pod Autoscaler together? Keep them off the same resource. Kubernetes recommends disabling HPA for any metric VPA already manages, since both react to the same signal and fight over it. VPA on CPU and memory beside HPA on a custom or external metric, like queue depth or requests per second, is the pairing that holds.

What CPU request should I set for a pod? Run the VPA with updateMode: "Off" and read the recommendation it stores. Size CPU to the target. Size memory to the upper bound, because a CPU request trimmed too far costs you latency while a memory request trimmed too far costs you a crash loop. Give it a full day covering your normal peak before you trust the numbers.

Does right-sizing change a pod's QoS class? It can, and that class decides who gets evicted first when a node runs short. A pod earns the Guaranteed class only when requests equal limits for both CPU and memory on every one of its containers. Drop a single request below its limit and the pod becomes Burstable, which is evicted ahead of Guaranteed pods. For a latency-sensitive service, hold requests equal to limits and take the saving on the pods behind it.

How OhChimp approaches this

The per-pod arithmetic is the easy half. The half that decides your invoice is whether every leftover pod on a drained node can actually be evicted, and that answer changes the moment someone adds a PodDisruptionBudget or an emptyDir.

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.

All OhChimp posts