Kubernetes Cost Optimization: The Complete Playbook
Most Kubernetes bills are substantially waste — not because the cluster is doing too much, but because pods reserve far more CPU and memory than they ever use. The scheduler carves out capacity based on requests, your cloud provider bills you for the nodes that back those requests, and actual usage is often a fraction of what was reserved. This playbook is a prioritized, hands-on guide to Kubernetes cost optimization: close the requests-versus-usage gap first, then autoscale, bin-pack, buy cheaper compute, kill zombies, and make cost visible so it stays down.
The core insight: you pay for requests, not usage
Kubernetes schedules pods onto nodes by subtracting each pod's resource requests from the node's allocatable capacity. Once a node's requests are "full," the autoscaler adds another node — even if actual CPU usage on the existing nodes is sitting at 15%. Your cloud invoice tracks node-hours, and node count tracks requested capacity. So the money is in the gap between what pods request and what they actually consume.
In practice, that gap is enormous. Engineers set requests once, during initial deployment, usually by copying another manifest or guessing generously "to be safe." Nobody revisits them. A typical cluster we audit runs at 20–40% actual CPU utilization against near-100% requested — meaning roughly half or more of the compute bill is reserving capacity nobody uses. Every optimization below is downstream of this one fact: shrink the reservation, shrink the bill.
Want your own number first?
Before reading six sections of strategy, get a baseline: the free calculator estimates how much of your monthly cloud bill is waste in sixty seconds — and the free audit measures it for real, with a prioritized fix list.
1. Right-size requests and limits (highest ROI, do this first)
Get the semantics straight before touching numbers:
- Requests are what the scheduler reserves for the pod. They determine placement and node count. This is the number that costs money.
- Limits are the ceiling. A container exceeding its CPU limit gets throttled — slower, but alive. A container exceeding its memory limit gets OOMKilled, because memory is not compressible: the kernel cannot take allocated pages back, so it kills the process instead.
That asymmetry drives the tuning strategy: be aggressive with CPU requests (worst case is throttling under contention), be careful with memory (worst case is a crash loop). A sane starting policy is CPU request near the p95 of actual usage with a generous or absent CPU limit, and memory request at p99 actual plus headroom, with the limit set equal to the request so behavior is predictable.
To find actual usage, start with kubectl top pods for a snapshot, but make decisions from Prometheus history — you want a week or more of data covering peak traffic, not a Tuesday-afternoon sample. Better yet, let the Vertical Pod Autoscaler do the math for you in recommendation-only mode, where it observes usage and publishes suggested requests without ever evicting a pod:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-recommender
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api
updatePolicy:
updateMode: "Off" # recommend only, never evictThen read the recommendations and apply them in your manifests deliberately:
kubectl describe vpa api-recommender
# Look at status.recommendation.containerRecommendations:
# lowerBound / target / upperBound per containerRoll changes out gradually — one service at a time, watching OOMKill and throttling metrics for a few days before moving on. Right-sizing one over-provisioned fleet of pods routinely frees whole nodes.
2. Autoscale everything: pods, nodes, and down to zero
Right-sizing sets the correct footprint per replica; autoscaling makes the replica and node counts track demand. Nodes you do not need should not exist — the cheapest node is the one that was scaled away an hour ago.
- HPA (Horizontal Pod Autoscaler) scales replicas. Pick a metric that actually reflects load: CPU works for CPU-bound services, but request rate, queue depth, or latency (via custom metrics) is often more honest. An HPA scaling on a metric that never moves is decoration.
- Cluster Autoscaler or Karpenter scales nodes. Cluster Autoscaler works everywhere; Karpenter (AWS) is notably better at consolidation — it actively replaces several under-utilized nodes with one right-sized node and picks instance types per pending workload. If you are on EKS, Karpenter with consolidation enabled is one of the highest-leverage single changes available — we break down exactly why in Karpenter vs Cluster Autoscaler.
- KEDA handles scale-to-zero for event-driven workers. A queue consumer with an empty queue should be zero pods, not three idle replicas. KEDA scales on queue length (SQS, RabbitMQ, Kafka lag, Redis) and wakes workloads only when there is work.
3. Bin-packing and node choice
The same total capacity costs different amounts depending on how it is shaped. Three levers:
- Fewer, larger nodes pack better. Every node carries fixed overhead — kubelet, kube-proxy, system daemons, OS reserve — and small nodes leave more unusable fragments. Ten pods fit into two 8-core nodes more efficiently than into eight 2-core nodes.
- Match instance families to workloads. Memory-heavy services on memory-optimized instances, compute-bound batch on compute-optimized. Running a cache fleet on general-purpose nodes means paying for CPU it never touches.
- Consider ARM. AWS Graviton (ARM) instances typically deliver meaningfully better price-performance than comparable x86. Most modern language runtimes and popular images ship multi-arch; the migration cost is usually rebuilding images with
--platform linux/arm64and testing. Azure and GCP have ARM equivalents.
4. Spot and preemptible instances
Spot (AWS), Spot VMs (Azure/GCP), and preemptible capacity commonly run 60–90% cheaper than on-demand, with the caveat that instances can be reclaimed with about two minutes' notice. That makes spot a poor fit for singletons and stateful primaries — and an excellent fit for anything replicated, stateless, or batch. Guardrails:
- Diversify across many instance types and zones so a single spot pool drying up does not take out your capacity. Karpenter and managed node groups both support this natively.
- Set PodDisruptionBudgets so evictions drain gracefully instead of killing all replicas of a service at once:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: api- Keep a small on-demand baseline for critical services and let spot absorb the burst and batch tiers. CI runners, queue workers, and rendering jobs are the classic first movers.
5. Reclaim idle and zombie resources
Every long-lived cluster accumulates debris that quietly bills forever:
- Idle namespaces — the proof-of-concept from last quarter, the departed teammate's test environment. Audit namespaces by actual traffic and delete or scale to zero.
- Over-provisioned non-prod. Dev and staging rarely need production-sized replicas or nodes — and they almost never need to run at 3 a.m. A CronJob or scheduled pipeline that scales non-prod deployments to zero overnight and on weekends cuts those environments' compute cost by more than half with zero user impact.
- Orphaned PersistentVolumes and load balancers. Deleting a Deployment does not delete its PVCs, and a forgotten Service of type LoadBalancer keeps a cloud LB (with an hourly charge) alive indefinitely. Sweep for unbound or Released PVs and Services with no backing pods.
- Snapshots and old images in registries and volume snapshot stores — small individually, meaningful in aggregate.
6. Make cost visible — or it all regresses
Everything above decays without feedback. Engineers who never see the cost of their namespace will re-inflate requests within a quarter. Instrument spend the way you instrument latency:
- Deploy OpenCost (the open-source core of Kubecost) or Kubecost itself to allocate cost per namespace, per team, and per workload. Put cost-per-namespace on the same dashboards teams already look at, and alert when a namespace's spend jumps week over week.
- Use PromQL to continuously surface the requests-versus-usage gap. For example, memory requested but not used, per pod:
sum by (namespace, pod) (
kube_pod_container_resource_requests{resource="memory"}
)
-
sum by (namespace, pod) (
container_memory_working_set_bytes{container!=""}
)Sort descending and you have a ranked hit list of the most over-requested pods in the cluster. The same pattern with rate(container_cpu_usage_seconds_total[5m]) against CPU requests finds the CPU offenders. Review the top ten monthly; it takes an hour and keeps the gap closed.
7. Guardrails: quotas and LimitRanges so waste cannot creep back
Right-sizing is a cleanup; quotas are the fence that keeps the yard clean. Two built-in objects do most of the work:
- ResourceQuota caps the total CPU, memory, and object counts a namespace can request. It turns "requests are free, ask for whatever" into a budget each team has to live inside — and makes a team's growth a deliberate conversation instead of silent node sprawl.
- LimitRange sets default and maximum requests/limits per container in a namespace. Deployments that ship without resource specs get sane defaults instead of the cluster's mercy, and nobody can request 16 CPU "to be safe" without tripping the max.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-a-quota
namespace: team-a
spec:
hard:
requests.cpu: "20"
requests.memory: 64Gi
services.loadbalancers: "2"
---
apiVersion: v1
kind: LimitRange
metadata:
name: team-a-defaults
namespace: team-a
spec:
limits:
- type: Container
defaultRequest: { cpu: 100m, memory: 128Mi }
default: { memory: 256Mi }
max: { cpu: "4", memory: 8Gi }Size quotas from measured usage plus reasonable headroom, not from what teams currently request — otherwise you enshrine the waste you just cleaned up. Pair them with an admission policy (Kyverno or a validating webhook) that rejects workloads with no requests at all, and the requests-versus-usage gap stops being able to silently reopen.
8. The bill outside compute: storage, network, observability
Nodes dominate most Kubernetes bills, but three line items grow quietly until someone looks:
- Cross-zone traffic. Most clouds charge for data crossing availability zones, and a chatty microservice mesh spread across three zones pays that toll on every hop. Enable topology-aware routing so services prefer same-zone endpoints, and keep high-volume pairs (app ↔ cache, app ↔ queue) zone-aligned where the availability trade-off is acceptable.
- Storage class and volume hygiene. On AWS, migrating gp2 volumes to gp3 is roughly 20% cheaper for the same or better baseline performance — a pure win that many clusters still have not taken. Also check for volumes provisioned at premium IOPS tiers for workloads that never use them.
- Logs and metrics. Shipping every debug log to a managed log service, or scraping high-cardinality metrics nobody queries, can cost more than the workloads being observed. Set retention deliberately, drop noisy labels, and sample where fidelity is not needed.
9. Commitment discounts come last — on purpose
Savings Plans, Reserved Instances, and committed-use discounts (30–60%+ off on-demand for a 1–3 year commitment) are real money — but they lock in your current shape. Commit before right-sizing and you are pre-paying for the waste. The correct order: right-size, autoscale, and consolidate first, let the cluster settle for a month, then cover the stable baseline — the compute floor that survives autoscaling troughs — with commitments, and let Spot and on-demand absorb everything above it.
Your Monday morning action list
- Run the PromQL above (or install OpenCost) and rank pods by requested-minus-used. Measure the gap before changing anything — it is your baseline and your business case.
- Deploy VPA in
updateMode: "Off"on your five largest deployments and right-size them from its recommendations over the next two weeks. - Enable node autoscaling with consolidation — Karpenter if you are on AWS — so freed capacity actually turns into fewer nodes.
- Scale non-prod to zero outside working hours.
- Sweep for orphaned PVs, idle namespaces, and unattached load balancers; delete them.
- Move one fault-tolerant workload (CI runners or queue workers) to spot with a PDB, then expand.
- Put cost-per-namespace on a dashboard and set a weekly spend-jump alert so it never creeps back.
Teams that work this list top to bottom typically cut a third or more of their Kubernetes spend in the first month, with the right-sizing pass alone doing most of the work. Prefer it as a checklist? The whole playbook is condensed into our 24-point Kubernetes cost optimization checklist — and if you run on AWS, the EKS-specific playbook covers Karpenter, Graviton, Savings Plans and the data-transfer traps in detail.
How much is your cluster wasting?
Put your own numbers behind these strategies. The free calculator estimates your monthly waste in sixty seconds — and the free audit replaces the estimate with your real figure and a prioritized fix list.
Frequently asked questions
Why are Kubernetes costs so high?
Kubernetes costs are high because you pay for reserved capacity, not actual usage. The scheduler places pods by their CPU/memory requests, and your cloud bills for the nodes backing those requests — but engineers typically set requests 2–5× above real usage and never revisit them. Add always-on non-production environments, missing autoscaling and orphaned volumes and load balancers, and 30–50% of a typical cluster bill buys nothing.
How do I reduce Kubernetes costs?
In priority order: (1) right-size CPU/memory requests against measured usage (use VPA in recommendation mode); (2) enable pod autoscaling (HPA/KEDA) and node autoscaling with consolidation (Karpenter on AWS); (3) move fault-tolerant workloads to Spot instances; (4) scale non-production environments to zero outside working hours; (5) delete orphaned volumes, load balancers and idle namespaces; (6) cover the stable baseline with Savings Plans or committed-use discounts.
What is the most effective way to manage Kubernetes costs?
Continuous visibility plus right-sizing. Deploy a cost-allocation tool such as OpenCost or Kubecost so every team sees the cost of its namespaces, alert on week-over-week spend jumps, and review the top over-requested workloads monthly. One-off cleanups regress within a quarter; the requests-versus-usage gap only stays closed when someone is looking at it.
How much can you save with Kubernetes cost optimization?
Most teams cut 30–50% of their Kubernetes spend in the first one to two months. Right-sizing over-requested workloads alone typically recovers 20–30%, node consolidation and Spot add 10–20% more, and shutting down idle non-production environments cuts those environments’ cost by 60–70%. Clusters that have never been optimized sit at the high end of the range.
What are Kubernetes cost optimization best practices?
Set resource requests from measured usage, not guesses; give every container a memory limit but be generous with CPU limits; autoscale pods (HPA/KEDA) and nodes (Karpenter/Cluster Autoscaler) with consolidation on; run fault-tolerant workloads on Spot behind PodDisruptionBudgets; enforce ResourceQuota and LimitRange per namespace; shut non-prod down out of hours; sweep orphaned PVs and load balancers; and make cost per namespace visible on team dashboards.
If you want an experienced pair of hands to run the audit and the rollout, our Kubernetes consulting engagements do exactly this — and pairing it with proper observability keeps the savings from quietly eroding once the initial push is done.
Related: Kubernetes cost
- Kubernetes Cost Optimization Checklist: 24 Checks That Cut Your Bill
- How to Cut Your EKS Bill: An AWS-Specific Playbook
- Karpenter vs Cluster Autoscaler: Which Saves More Money?
- EKS vs AKS vs GKE: Which Should You Choose in 2026?
- Managed Kubernetes vs In-House: The Real Cost Comparison
- Cloud Cost Waste Calculator — estimate your overspend in 60 seconds
- Free DevOps audit — get your real waste number, measured
Need this done, not just read about?
Deplyra builds, ships and runs exactly this in production — as code, with GitOps, handed over documented.
Start a project →