Kubernetes Cost Optimisation: Cut Your Cloud Bill by 40%
TL;DR: Most Kubernetes clusters run 40–60% overprovisioned. Right-size requests and limits with real usage data, move stateless and batch workloads onto spot/preemptible nodes, scale the cluster itself with Cluster Autoscaler, and shut down non-production environments outside working hours. Combined, these four changes routinely cut K8s spend by 30–60% with no performance impact.
Cloud costs spiral fast when Kubernetes clusters are left unoptimised. We've helped clients cut their K8s spend by 30–60% without any impact on performance.
The pattern is almost always the same: a cluster gets stood up with generous defaults so nothing breaks under launch-day traffic, those defaults are never revisited once things stabilise, and every new service copies the same oversized template. None of that is negligence — it's just that nobody owns "go back and right-size this" as an ongoing job. The four changes below turn it into one.
Here's the playbook:
1. Right-Size Your Nodes and Pods
Most teams overprovision by default — a pod requesting 1 CPU and 2Gi of memory "just in case" is common even when it uses a fraction of that. Use the Vertical Pod Autoscaler (VPA) in recommendation mode to see real usage before touching limits:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: web-vpa
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: web
updatePolicy:
updateMode: "Off" # recommendation only, no auto-changes yet
Check kubectl describe vpa web-vpa after a few days of traffic — we typically find 40–60% of pods are overprovisioned once real numbers are in front of the team.
2. Use Spot / Preemptible Instances for Non-Critical Workloads
Batch jobs, dev environments, and stateless web workloads can tolerate interruption — and spot instances cost 60–90% less than on-demand. Taint a spot node pool and target it explicitly:
apiVersion: apps/v1
kind: Deployment
metadata:
name: batch-worker
spec:
template:
spec:
tolerations:
- key: "cloud.google.com/gke-spot"
operator: "Equal"
value: "true"
effect: "NoSchedule"
nodeSelector:
cloud.google.com/gke-spot: "true"
containers:
- name: worker
image: ekamops/batch-worker:latest
Keep stateful workloads (databases, message queues with local disk state) on regular nodes — spot interruption for those causes more pain than it saves in cost.
3. Scale the Cluster, Not Just the Pods
Horizontal Pod Autoscaler (HPA) scales pod count, but if the cluster itself doesn't shrink, you keep paying for idle nodes overnight. Pair HPA with the Cluster Autoscaler so nodes scale down when pods do:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65
Set minReplicas low enough to actually shrink during off-peak hours — a floor of 10 replicas on a service that needs 2 overnight is paying for 8 idle pods every single night.
4. Shut Down Non-Production Environments on a Schedule
Dev and staging clusters running 24/7 at full size are pure waste outside business hours. A simple CronJob scaling deployments to zero overnight covers most of the savings without deleting anything:
apiVersion: batch/v1
kind: CronJob
metadata:
name: scale-down-dev
spec:
schedule: "0 20 * * 1-5" # 8pm weekdays
jobTemplate:
spec:
template:
spec:
containers:
- name: kubectl
image: bitnami/kubectl
command:
- kubectl
- scale
- deployment
- --all
- --replicas=0
- -n
- dev
restartPolicy: Never
Pair it with a matching CronJob that scales back up at 8am — a dev cluster running 12 hours a day instead of 24 is an immediate 50% compute cost cut for that environment alone.
Choosing What to Measure First
Optimising blind is how teams cut the wrong 10% and leave the real 50% untouched. Before changing anything, tag every workload by team and environment so cost can actually be attributed, then look at three numbers: requested vs. actual CPU/memory usage per pod, node utilisation across the whole cluster, and the ratio of production to non-production spend. That last one is often the biggest surprise — non-prod environments regularly cost as much as production because nobody ever revisits them after the initial setup.
Tooling for Ongoing Cost Visibility
Right-sizing once and never checking again means costs creep back within a quarter as new services get added at default resource sizes. Kubecost (or its open-source core, OpenCost) gives a per-namespace, per-deployment cost breakdown from inside the cluster itself:
helm repo add kubecost https://kubecost.github.io/cost-analyzer/
helm install kubecost kubecost/cost-analyzer
--namespace kubecost --create-namespace
--set kubecostToken="your-token-here"
Point engineering leads at the Kubecost dashboard directly instead of routing every cost question through the platform team — visibility is what turns right-sizing into an ongoing habit instead of a one-time project that quietly unravels.
Making Savings Stick with Governance
The most common failure mode isn't finding the savings — it's losing them again a few months later as new services launch without limits. Two guardrails prevent that:
- Resource quotas per namespace — cap the total CPU/memory a team's namespace can request, so "just set it high to be safe" has a hard ceiling.
- Budget alerts — a cloud billing alert at 80% of the monthly target catches a runaway workload within days, not at the end of the month when the invoice arrives.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: team-checkout
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
What "No Performance Impact" Actually Means
Right-sizing only stays safe if it's based on real usage data, not guesswork — that's the entire point of running VPA in recommendation mode first instead of jumping straight to smaller limits. Set requests close to the P90 of actual usage, not the average, since the average hides the spikes, and keep a modest buffer on limits so a traffic spike gets throttled gracefully instead of OOM-killed. Optimisation that ignores this and simply shrinks every request by a flat percentage is how cost cuts turn into production incidents.
Combining Spot with Reserved Capacity
Spot instances are the biggest single lever, but they aren't the whole answer — a cluster running entirely on spot capacity is exposed if a region-wide spot shortage hits every non-critical workload at once. A more resilient split is reserved or committed-use capacity (1- or 3-year commitments) sized to your steady-state baseline load, with spot layered on top for burst and non-critical work, and a small slice of regular on-demand nodes kept as a fallback for anything spot can't fulfil in time. The reserved layer alone typically saves 30–50% over pure on-demand pricing for the portion of load that never actually varies, and it's the piece teams skip most often simply because right-sizing and spot get the attention first.
Putting It Together
None of these four changes is individually dramatic, but they compound: right-sizing removes the padding, spot instances discount what's left, cluster autoscaling stops paying for idle capacity, and scheduled shutdowns eliminate off-hours waste entirely. Clients who apply all four together land in the 30–60% range — the exact figure depends on how overprovisioned the starting cluster was and how much of the workload is genuinely non-critical.
Where to Start
- Week 1 — deploy VPA in recommendation mode across all deployments, no changes yet.
- Week 2 — apply right-sized requests/limits based on VPA data, starting with the least critical services.
- Week 3 — move batch and dev workloads to spot node pools.
- Week 4 — add Cluster Autoscaler and scheduled shutdowns for non-prod environments.
Want a free audit of where your cluster is overspending? Talk to EkamOps — we'll show you the numbers before you commit to anything.
Want help applying this to your stack?
Book a free consultation →