Kubernetes Cost Optimization: How to Cut Cloud Spend by 60% Without Touching Your Application Code
Most engineering teams overpay for Kubernetes by 40–70% without realizing it. This deep-dive shows you the exact infrastructure patterns, tooling, and scheduling strategies to reclaim that spend — without a single line of app-level change.
TL;DR Quick Answer: Kubernetes cost optimization is the practice of right-sizing workloads, eliminating idle capacity, leveraging spot/preemptible instances, and using intelligent autoscalers (Karpenter, VPA, KEDA) to reduce cloud infrastructure spend by 40–60% — without modifying your application code. The biggest wins come from fixing resource requests, adopting bin-packing schedulers, and killing zombie workloads that silently drain budgets.
The Silent Budget Drain Inside Every Kubernetes Cluster
If your team runs production workloads on Kubernetes, there is a near-certainty you are overpaying. Kubernetes cost optimization is not a niche concern reserved for hyper-scale companies — it is a survival skill for any engineering team that wants to stay profitable as it grows. Studies from CNCF and Datadog's State of Cloud Costs report consistently show that the average Kubernetes cluster runs at only 13–20% actual CPU utilization against provisioned capacity. That means for every $100,000 you spend on cloud compute, roughly $80,000 is buying you headroom you never use.
At Apargo, we have audited dozens of production clusters for clients across SaaS, fintech, and e-commerce verticals. The pattern is almost always the same: over-requested resources, static node pools, and no meaningful autoscaling strategy. In this article, we break down every lever available to you — with real configuration examples — to slash that bill without touching a single line of application code.
Why Kubernetes Cost Optimization Is Structurally Hard
Before diving into solutions, it is worth understanding why clusters become expensive in the first place. Kubernetes was designed for reliability and availability — not cost efficiency. Its default scheduling behavior is conservative, and most teams inherit configurations that prioritize "never go down" over "never overspend."
The Four Root Causes of Kubernetes Overspend
- Inflated resource requests: Developers set CPU/memory requests based on peak load or fear, not actual profiling. A service that uses 100m CPU at p99 often has a 1000m request.
- Static node pools: Fixed node groups provisioned for peak traffic sit idle 80% of the time, especially for batch or overnight workloads.
- Namespace sprawl and zombie workloads: Old staging environments, forgotten cronjobs, and abandoned deployments quietly consume resources across namespaces.
- No bin-packing awareness: Default Kubernetes scheduling spreads pods across nodes for reliability, leaving many nodes at 30–40% utilization — not enough to scale down, too little to justify the cost.
Strategy 1 — Right-Sizing Resource Requests with VPA
The single highest-ROI action in any Kubernetes cost optimization effort is fixing resource requests. The Vertical Pod Autoscaler (VPA) from the Kubernetes autoscaler project can profile your workloads and recommend — or automatically apply — accurate CPU and memory requests based on real usage data.
Install VPA in recommendation-only mode first to avoid disrupting running pods:
# Install VPA components (recommendation mode only)
kubectl apply -f https://github.com/kubernetes/autoscaler/releases/latest/download/vertical-pod-autoscaler.yaml
# VPA object for a sample API deployment
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-service-vpa
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api-service
updatePolicy:
updateMode: "Off" # Recommendation only — safe for production audit
resourcePolicy:
containerPolicies:
- containerName: api
minAllowed:
cpu: 50m
memory: 64Mi
maxAllowed:
cpu: 2
memory: 2Gi
controlledResources: ["cpu", "memory"]
After 24–48 hours, query VPA recommendations:
# Fetch VPA recommendations for all objects in namespace
kubectl get vpa -n production -o json | \
jq '.items[] | {name: .metadata.name, recommendations: .status.recommendation}'
In our client audits, applying VPA recommendations alone has reduced cluster node count by 25–35%, translating directly to proportional cost savings. One SaaS platform we worked with dropped from 42 nodes to 27 nodes within two weeks of applying right-sized requests — a $14,000/month reduction with zero application changes.
Strategy 2 — Replace Cluster Autoscaler with Karpenter
The traditional Kubernetes Cluster Autoscaler works well but has a fundamental limitation: it scales within predefined node groups. Karpenter, the open-source node provisioner from AWS, takes a radically different approach — it provisions the exact instance type that fits pending pods, rather than scaling a pre-configured group.
Why Karpenter Wins on Cost
- Provisions nodes in under 60 seconds vs. 3–5 minutes for Cluster Autoscaler
- Selects the cheapest compatible instance type automatically across families (m5, m6i, m6a, etc.)
- Consolidates underutilized nodes aggressively via Disruption Budgets
- Natively supports mixed spot/on-demand provisioning per workload priority
# Karpenter NodePool — cost-optimized with spot preference
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: cost-optimized-pool
spec:
template:
metadata:
labels:
billing-team: platform
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"] # Spot preferred, on-demand fallback
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"] # ARM (Graviton) is 20% cheaper
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["m", "c", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["5"] # Modern generation only
nodeClassRef:
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenUnderutilized
consolidateAfter: 30s # Aggressive bin-packing
limits:
cpu: 1000
memory: 4000Gi
Enabling Karpenter with this configuration on a mid-sized production cluster typically yields a 20–30% additional cost reduction on top of right-sizing wins, primarily through spot instance adoption and aggressive node consolidation.
Strategy 3 — Spot Instance Architecture for Stateless Workloads
Spot instances (AWS) or preemptible VMs (GCP) offer 60–90% discount over on-demand pricing. The catch is they can be reclaimed with a 2-minute warning. For stateless, horizontally-scalable services — which describes most microservice architectures — this is an entirely acceptable tradeoff.
Engineering Spot Tolerance Into Your Deployments
The key is designing for graceful disruption. Your pods must handle SIGTERM cleanly and your deployments must have enough replicas that a single node loss is non-catastrophic:
# Deployment configured for spot tolerance
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker-service
spec:
replicas: 6 # Never run fewer than 3 replicas on spot
strategy:
rollingUpdate:
maxUnavailable: 1 # Only 1 pod can be unavailable at a time
maxSurge: 2
template:
spec:
terminationGracePeriodSeconds: 60 # Give pods 60s to drain
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule # Spread across nodes
labelSelector:
matchLabels:
app: worker-service
tolerations:
- key: "karpenter.sh/capacity-type"
operator: "Equal"
value: "spot"
effect: "NoSchedule"
containers:
- name: worker
image: your-registry/worker-service:latest
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"] # Drain in-flight requests
A practical split that balances cost and reliability for most production workloads: 70% spot, 30% on-demand, with critical stateful services (databases, message brokers) always on on-demand.
Strategy 4 — KEDA for Event-Driven Scale-to-Zero
Kubernetes Event-Driven Autoscaling (KEDA) lets you scale workloads based on external signals — queue depth, Kafka lag, HTTP request rate, cron schedules — and crucially, scale all the way to zero replicas when there is no work to do.
For batch processors, async workers, and scheduled jobs, scale-to-zero can eliminate 60–80% of compute cost for those workload types entirely:
# KEDA ScaledObject — scale SQS consumer to zero when queue is empty
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: sqs-worker-scaler
namespace: production
spec:
scaleTargetRef:
name: sqs-worker-deployment
minReplicaCount: 0 # True scale-to-zero
maxReplicaCount: 50 # Burst capacity
cooldownPeriod: 300 # 5 min cooldown before scaling down
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.us-east-1.amazonaws.com/123456789/jobs-queue
queueLength: "5" # 1 replica per 5 messages
awsRegion: us-east-1
scaleOnInFlight: "true"
Combined with Karpenter's fast node provisioning (sub-60s), KEDA scale-to-zero means your batch infrastructure costs nothing when idle — which for most teams is 14–18 hours per day.
Strategy 5 — Namespace Cost Attribution and Zombie Hunting
You cannot optimize what you cannot measure. Kubernetes cost optimization requires visibility into which teams, services, and namespaces are consuming what resources. Tools like Kubecost or OpenCost (the CNCF-donated open-source version) provide per-namespace, per-deployment cost breakdowns with 15-minute granularity.
Setting Up OpenCost for Free Cost Visibility
# Install OpenCost via Helm
helm repo add opencost https://opencost.github.io/opencost-helm-chart
helm repo update
helm install opencost opencost/opencost \
--namespace opencost \
--create-namespace \
--set opencost.exporter.cloudProviderApiKey="your-aws-pricing-key" \
--set opencost.ui.enabled=true
Once instrumented, set up automated alerts for:
- Namespaces with zero traffic for more than 72 hours (zombie environments)
- Deployments with CPU utilization below 5% of their request for 7+ days
- PersistentVolumes that are unattached (orphaned storage still billed at full rate)
- LoadBalancer services with no active endpoints (idle LB charges add up fast)
In a recent audit for an e-commerce platform, we identified 11 forgotten staging namespaces and 23 orphaned PersistentVolumes totaling $3,200/month in completely wasted spend. Cleaning those up took one afternoon.
Strategy 6 — ARM/Graviton Nodes for a Free 20% Discount
AWS Graviton3 (arm64) instances are consistently 20% cheaper than equivalent x86 instances and often 40% more energy-efficient. For containerized workloads using standard base images, migration is often as simple as building a multi-arch image and updating your NodePool selector.
# Multi-arch Docker build for ARM + x86 support
docker buildx create --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag your-registry/api-service:latest \
--push .
Most Go, Node.js, Python, and JVM workloads run on ARM without any code changes. The Kubernetes cost optimization gain here is passive — just rebuild your images as multi-arch and let Karpenter pick Graviton nodes when they are cheaper.
Strategy 7 — LimitRanges and ResourceQuotas to Prevent Future Overspend
The best Kubernetes cost optimization is the overspend that never happens. LimitRanges enforce default and maximum resource constraints at the namespace level, preventing developers from accidentally deploying with no limits
Related Articles
Explore more insights from our engineering and product teams.
