A Kubernetes bill is mostly nodes, and nodes are mostly capacity you reserved but never used. Cost optimization on Kubernetes isn't one clever trick — it's a ladder of them, from the individual pod all the way up to how many clusters and availability zones you run. This playbook walks the whole ladder in the order that gives the most saving for the least risk, so you can start at the top and stop whenever the numbers are good enough.
First principle: requests are the currency
In Kubernetes you do not pay for what your pods use — you pay for what they request. A CPU or
memory request reserves capacity on a node and drives how many nodes the scheduler must keep
around. A pod that requests 2 CPU but uses 0.1 still ties up two CPUs' worth of a node that
someone is paying for. So the single biggest lever in the entire game is closing the gap between
requests and real usage.
Everything below is, ultimately, a way to either lower what you request or lower the price of the capacity that backs those requests.
The industry reality check: most Kubernetes clusters run at 10–25% average CPU utilisation. That means roughly three-quarters of the bill is capacity nobody is using — reserved by inflated requests, held open by static node pools sized for peak, and duplicated across environments that don't need it. The good news is that the same three-quarters is exactly what the steps below reclaim, and you can do it incrementally: nothing here is all-or-nothing, and the earliest steps are also the safest.
Step 1 — Right-size your pods (the biggest, safest win)
Start with data, never guesses. Compare real usage to requests:
kubectl top pods -A # quick live view
# In Prometheus, the gap that costs you money:
# sum(kube_pod_container_resource_requests{resource="cpu"})
# - sum(rate(container_cpu_usage_seconds_total[5m]))
Then set requests to roughly the p95 of real usage plus a little headroom. If a service peaks at
300m CPU / 400Mi, request 350m / 512Mi — not the 1 CPU / 1Gi someone copy-pasted a year ago.
Two rules that matter:
- CPU is compressible; memory is not. It's usually fine to set a CPU request and skip the CPU limit (a CPU limit just throttles). But set a memory limit close-ish to the request, because exceeding it gets the pod OOM-killed.
- Let the Vertical Pod Autoscaler (VPA) do the measuring for you in recommendation mode:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api
updatePolicy:
updateMode: "Off" # "Off" = just recommend; read kubectl describe vpa api
Right-sizing requests routinely cuts node count 20–40% on its own, with essentially zero risk.
Step 2 — Let the platform size the nodes
Once requests are honest, stop hand-managing node pools. Modern schedulers provision right-sized nodes just-in-time from your pending pods and delete them when empty:
- GKE Node Auto-Provisioning (NAP) creates and removes node pools automatically to fit pending workloads, and consolidates under-used nodes.
- EKS Auto Mode (managed Karpenter) does the same on AWS: it looks at unschedulable pods, launches the cheapest node shape that fits, and bin-packs / consolidates aggressively.
- Plain Cluster Autoscaler is the older, static-node-pool approach — better than nothing, but it can only scale pools you pre-defined.
The win is matching supply to demand: no more half-empty n2-standard-8s sitting idle overnight
because someone sized the pool for peak.
Step 3 — Compute classes per namespace (GKE) — spot-first with a safe fallback
GKE Custom Compute Classes let you declare an ordered list of preferences — try the cheapest option first, fall back automatically when it's unavailable — and apply it per namespace or per workload. This is where Spot becomes safe to use by default:
apiVersion: cloud.google.com/v1
kind: ComputeClass
metadata:
name: cost-optimized
spec:
priorities:
- spot: true # 1st choice: cheapest — Spot ARM
machineFamily: t2a
- spot: true # 2nd: Spot x86
machineFamily: n2
- spot: false # 3rd: on-demand fallback so pods never get stuck
machineFamily: n2
activeMigration:
optimizeRulePriority: true # migrate back up to a cheaper tier when it returns
nodePoolAutoCreation:
enabled: true
Apply it as a namespace default so every workload in, say, batch inherits spot-first behaviour
without editing a single Deployment:
apiVersion: v1
kind: Namespace
metadata:
name: batch
labels:
cloud.google.com/default-compute-class: cost-optimized
optimizeRulePriority is the piece people forget: after a Spot stock-out forces a fallback to
on-demand, it migrates workloads back down to Spot when capacity returns, so you don't quietly pay
full price forever. (We have a dedicated deep-dive article on the Spot-first / regular-fallback
pattern if you want the full lab.)
Step 4 — Move stateless and batch workloads to ARM64
ARM nodes — AWS Graviton, GCP Tau T2A / Axion, Azure Ampere — deliver roughly 20–40% better price-performance than comparable x86. The only real requirement is multi-arch container images:
docker buildx build --platform linux/amd64,linux/arm64 -t registry/app:1.4 --push .
Most base images (Alpine, distroless, the official language images) are already multi-arch, so many apps "just work." Steer workloads onto ARM with a nodeSelector (or let a compute class prefer it):
spec:
nodeSelector:
kubernetes.io/arch: arm64
ARM is ideal for stateless services, HTTP APIs, batch jobs and CI build agents. Before you migrate a component, confirm its native dependencies (some databases, ML wheels, proprietary agents) ship ARM builds.
Step 5 — Make Spot safe, then use it everywhere it fits
Spot / preemptible capacity is 60–90% cheaper but can be reclaimed on short notice (an involuntary disruption). Make a workload spot-safe and you unlock those savings:
- Run 2+ replicas spread across nodes (
topologySpreadConstraints). - Add a PodDisruptionBudget and graceful
SIGTERMhandling so a reclaim drains cleanly. - Checkpoint long-running jobs so a preemption costs minutes, not hours.
- Keep a small on-demand baseline for anything quorum-based; never place all members of a Raft cluster on Spot.
Stateless services and batch are perfect Spot candidates. (For why a PDB does not save you from Spot reclamation, see our PodDisruptionBudgets article — it's a common and expensive misunderstanding.)
Step 6 — Non-prod is where the easy money is
Development, staging and CI environments often mirror production at full cost for no reason. This is usually the fastest 30–50% off the total bill:
- Fewer clusters. Consolidate dev + staging into one cluster split by namespace with ResourceQuotas, instead of a cluster each (every cluster carries a control-plane fee and its own system overhead).
- Fewer availability zones. Production wants 3 zones for HA; dev needs one. A single-zone non-prod cluster cuts cross-zone data-transfer charges and the per-zone node minimums.
- Scale to zero off-hours. Roughly 65% of a week is outside 9–6, Monday–Friday. A scheduled
down-scaler (e.g.
kube-downscaler, or a simple CronJob) that scales dev deployments and node pools to zero at night and on weekends is close to free money:
# annotate dev workloads so a downscaler parks them 20:00–08:00 and all weekend
metadata:
annotations:
downscaler/uptime: "Mon-Fri 08:00-20:00 Asia/Karachi"
- Spot-only for non-prod. Non-prod can tolerate the occasional preemption, so run it (almost) entirely on Spot.
- Smaller everything — one replica, smaller requests, shorter log/metric retention.
Step 7 — Scale pods to demand (and to zero)
- HPA scales replicas on CPU/memory or custom metrics; combined with node auto-provisioning, idle services shrink their node footprint automatically.
- KEDA adds event-driven and scale-to-zero autoscaling — a worker with an empty queue runs no pods, and the autoscaler then removes the empty node. No traffic, no cost.
Step 8 — Measure, or you're just guessing
You can't optimise what you can't see. Turn on cost attribution and watch two north-star metrics — requests-vs-usage gap and node utilisation — per namespace/team:
- GKE cost allocation / cost breakdown, AWS Split Cost Allocation Data for EKS, or the open-source OpenCost / Kubecost.
- Alert when a namespace's requested-but-unused CPU crosses a threshold; that's money on the floor.
Don't forget committed-use discounts on your baseline
Everything above lowers how much on-demand capacity you need. For the steady baseline that remains — the nodes that are always on — buy it at a discount. GCP Committed Use Discounts (CUDs), AWS Savings Plans / Reserved Instances, and Azure Reservations take 30–60% off compute you commit to for one or three years. The sequencing is what matters: right-size and consolidate first so you commit to your real floor, not your bloated one; then cover that floor with a commitment and let Spot plus autoscaling handle everything above it. Committing before you optimise just locks in the waste.
The costs that aren't compute
Nodes dominate the bill, but three line items quietly add up:
- Storage. Over-provisioned PersistentVolumes and forgotten released PVs bill every hour. Pick the right StorageClass (don't park cold data on premium SSD), enable volume expansion so you can start small, and reclaim orphaned disks.
- Network. Cross-zone and egress traffic is a real charge — a chatty service spread across three zones pays for every hop. Keep tightly-coupled workloads zone-aware in prod, collapse non-prod to one zone, and consolidate external load balancers (one shared ingress beats a LoadBalancer Service per app).
- Observability. Logs and high-cardinality metrics can cost more than the cluster they watch. Set sane retention, sample verbose logs, and drop metric labels nobody queries.
Anti-patterns that quietly burn money
- Copy-pasted requests. The
1 CPU / 1Giset "to be safe" on every deployment is the single most common cause of an oversized cluster. GuaranteedQoS everywhere. Settinglimits == requestson every pod maximises reserved (and billed) capacity. Reserve it for latency-critical workloads; let the rest burst.- Prod-grade non-prod. Three zones, five replicas and full retention in a dev cluster that's idle every night.
- Static node pools sized for peak. They pay for peak 24/7; auto-provisioning pays for peak only at peak.
- Spot with no fallback or no PDB. Either pods get stuck when Spot is scarce, or a reclaim storm takes a service down — both push teams to abandon Spot and lose the savings.
A worked example
A team runs a 12-node n2-standard-8 cluster (96 vCPU, ~$5,000/mo on-demand) sitting at 22%
average CPU. Walking the ladder:
- Right-size requests to p95 usage → the same pods now fit on 7 nodes (~40% off).
- Auto-provisioning + consolidation → node count flexes between 5 and 8 with load instead of a fixed 12 (a further ~15%).
- Non-prod (a second, identical cluster) collapses to one zone, goes Spot-only, and scales to zero nights and weekends → roughly 60% off that environment.
- Spot-first with on-demand fallback on the prod stateless tier → another ~30% on those nodes.
- Cover the remaining always-on floor with a 1-year CUD → ~35% off the baseline.
None of these steps is exotic, and together they routinely turn a $5,000 cluster into a $1,800–$2,200 one without touching reliability targets — because the capacity you removed was never doing anything in the first place.
The prioritised checklist (biggest ROI first)
| # | Action | Typical saving | Risk |
|---|---|---|---|
| 1 | Right-size pod requests from real usage | 20–40% | Low |
| 2 | Node auto-provisioning + consolidation (NAP / Auto Mode / Karpenter) | 15–30% | Low |
| 3 | Non-prod: fewer clusters/zones, off-hours scale-down, spot-only | 30–50% of non-prod | Low |
| 4 | Spot-first with on-demand fallback (compute classes) | 40–70% on eligible workloads | Medium |
| 5 | ARM64 for stateless & batch | 20–40% on those workloads | Medium |
| 6 | HPA/KEDA scale-to-zero + continuous measurement | ongoing | Low |
Work top-down and stop when the bill is where you want it. The top three items are low-risk and usually enough to cut a cluster's cost in half.
Where to go next
Our Advanced SRE & Observability course puts numbers on all of this — building the requests-vs-usage dashboards, setting utilisation targets, and running a real spot-first, ARM-preferred cluster with graceful preemption — so cost optimisation becomes a habit you can measure, not a one-off cleanup.