Kubernetes Cost Optimization: Reduce K8s Spending
In this tutorial, you'll learn about Kubernetes Cost Optimization: Reduce K8s Spending. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Kubernetes cost optimization is the practice of reducing K8s spending by right-sizing node pools, optimizing pod resource requests, using Cluster Autoscaler or Karpenter for dynamic scaling, leveraging spot instances, and eliminating resource waste with monitoring tools like Kubecost.
What You'll Learn
You'll configure node autoscaling with Cluster Autoscaler and Karpenter, set pod resource requests and limits that match actual usage, use Vertical Pod Autoscaler for automated right-sizing, implement namespace quotas, monitor costs with Kubecost, and reclaim unused resources.
Why It Matters
Kubernetes clusters are the most over-provisioned infrastructure in Cloud Computing. Teams set generous CPU and memory requests to avoid OOM kills, leave nodes running 24x7, and deploy workloads that could be scheduled on spot instances. The result: 40-60% of K8s spend is waste. DodaTech reduced EKS costs for DodaZIP's CI/CD platform by 55% using Karpenter with spot node pools and VPA recommendations.
flowchart TD
A[Cluster Audit] --> B{Pod Requests\nvs Usage}
B -->|Over-Requested| C[VPA Recommendations]
B -->|Under-Utilized Nodes| D[Node Right-Sizing]
C --> E[Update Resource Limits]
D --> F[Cluster Autoscaler]
D --> G[Karpenter]
F --> H[Spot Node Pools]
G --> H
H --> I[40-60% Savings]
E --> I
style I fill:#f59e0b,color:#fff
1. Pod Resource Requests and Limits
Setting realistic resource requests is the highest-impact K8s cost optimization. Most teams over-request by 2-5x.
# Audit current pod resource usage across a namespace
kubectl top pods -n production --containers
# Get detailed utilization metrics
kubectl describe nodes | grep -A 5 "Allocated resources"
# Use metrics server to see per-pod CPU and memory
kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/production/pods | jq '.items[] | {name: .metadata.name, cpu: .containers[0].usage.cpu, memory: .containers[0].usage.memory}'
Expected output:
NAME CPU(cores) MEMORY(bytes)
web-api-7d8f9c6b8f-2kxm5 45m 128Mi
web-api-7d8f9c6b8f-jn3p9 52m 135Mi
worker-6b8f9c7d8f-9k3m2 120m 256Mi
If your deployment requests 1000m CPU and uses 45m, you are over-requesting by 22x. That wastes an entire node's worth of capacity.
Setting Optimal Requests
# production-web.yaml — optimized resource requests
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-api
namespace: production
spec:
replicas: 3
template:
spec:
containers:
- name: web-api
image: dodatech/web-api:1.5
resources:
requests:
cpu: "100m" # Based on P95 usage of 52m + 50% buffer
memory: "200Mi" # Based on P95 usage of 135Mi + 50% buffer
limits:
cpu: "500m" # Burst cap
memory: "512Mi" # OOM protection
2. Cluster Autoscaler vs Karpenter
| Feature | Cluster Autoscaler | Karpenter |
|---|---|---|
| Scaling unit | Node group / ASG | Instance directly |
| Node diversity | Limited to ASG config | Multiple instance types |
| Scaling speed | Minutes | Seconds |
| Spot integration | Via mixed instances | Native spot prioritization |
| Consolidation | Manual | Automatic |
# Deploy Cluster Autoscaler on EKS
helm repo add autoscaler https://kubernetes.github.io/autoscaler
helm install cluster-autoscaler autoscaler/cluster-autoscaler \
--namespace kube-system \
--set autoDiscovery.clusterName=dodatech-eks \
--set awsRegion=us-east-1 \
--set extraArgs.scale-down-enabled=true \
--set extraArgs.scale-down-delay-after-add=10m \
--set extraArgs.scale-down-unneeded-time=10m
# Deploy Karpenter on EKS (faster, more efficient)
helm repo add karpenter https://charts.karpenter.sh
helm install karpenter karpenter/karpenter \
--namespace karpenter \
--create-namespace \
--set clusterName=dodatech-eks \
--set clusterEndpoint=$(aws eks describe-cluster --name dodatech-eks --query cluster.endpoint) \
--set defaultProvisioner.requirements[0].key=karpenter.sh/capacity-type \
--set defaultProvisioner.requirements[0].op=In \
--set defaultProvisioner.requirements[0].values=["spot"]
Karpenter Provisioner Configuration
# karpenter-provisioner.yaml
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: default
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: node.kubernetes.io/instance-type
operator: In
values:
- "m5.large"
- "m5.xlarge"
- "c5.large"
- "c5.xlarge"
- "r5.large"
limits:
resources:
cpu: 1000
provider:
subnetSelector:
Name: "*private*"
securityGroupSelector:
Name: "*eks*"
ttlSecondsAfterEmpty: 30
3. Vertical Pod Autoscaler (VPA)
VPA automatically adjusts CPU and memory requests based on historical usage.
# Install VPA
git clone https://github.com/kubernetes/autoscaler.git
kubectl apply -k autoscaler/vertical-pod-autoscaler/deploy/manifest/
# Create a VPA for a deployment
kubectl apply -f - <<EOF
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: web-api-vpa
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: web-api
updatePolicy:
updateMode: Auto
resourcePolicy:
containerPolicies:
- containerName: '*'
minAllowed:
cpu: "50m"
memory: "100Mi"
maxAllowed:
cpu: "2"
memory: "2Gi"
EOF
# Check VPA recommendations
kubectl describe vpa web-api-vpa -n production
Expected recommendation output:
Status:
Conditions:
State: Active
Recommendation:
Container Recommendations:
Name: web-api
Lower Bound:
Cpu: 75m
Memory: 180Mi
Target:
Cpu: 100m
Memory: 240Mi
Upper Bound:
Cpu: 400m
Memory: 600Mi
4. Namespace Resource Quotas and Limit Ranges
Prevent teams from over-consuming cluster resources with quotas.
# production-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: production
spec:
hard:
requests.cpu: "20"
requests.memory: "80Gi"
limits.cpu: "40"
limits.memory: "160Gi"
persistentvolumeclaims: 20
pods: 100
---
apiVersion: v1
kind: LimitRange
metadata:
name: production-limits
namespace: production
spec:
limits:
- max:
cpu: "4"
memory: "8Gi"
min:
cpu: "25m"
memory: "64Mi"
default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "200Mi"
type: Container
5. Kubecost Monitoring
Kubecost provides per-namespace, per-deployment, and per-label cost breakdowns.
# Install Kubecost
helm repo add kubecost https://kubecost.github.io/cost-analyzer/
helm install kubecost kubecost/cost-analyzer \
--namespace kubecost \
--create-namespace \
--set kubecostToken="dodatech" \
--set prometheus.nodeExporter.enabled=false \
--set global.zoneId="production"
# Expose port and view dashboard
kubectl port-forward --namespace kubecost deployment/kubecost-cost-analyzer 9090
# Query cost allocation via API
kubectl exec -n kubecost deploy/kubecost-cost-analyzer -- \
curl -s "http://localhost:9001/model/costAllocation?window=1d&aggregate=namespace"
# Get savings report
kubectl exec -n kubecost deploy/kubecost-cost-analyzer -- \
curl -s "http://localhost:9001/model/savingsScheduling?window=30d"
Expected API output:
{
"namespace": "production",
"totalCost": 4520.15,
"resources": {
"cpu": 2100.30,
"memory": 980.50,
"storage": 520.35,
"network": 320.00
},
"savings": {
"clusterRightSizing": 780.00,
"spotMigration": 1200.50,
"idleResources": 340.25
}
}
Common Mistakes
Setting CPU requests too high: Each millicore you request but don't use consumes space on a node that could run another pod. Audit and adjust monthly.
No cluster autoscaler: Fixed-size node pools run empty nodes at full cost. Always configure Cluster Autoscaler or Karpenter.
Ignoring spot instances: Kubernetes workloads are naturally fault-tolerant. Run spot node pools for all stateless workloads and use pod disruption budgets for graceful handling.
No namespace quotas: Without quotas, one team can consume the entire cluster. Set ResourceQuota and LimitRange per namespace.
Running monitoring infrastructure on on-demand nodes: Prometheus, Grafana, and Kubecost itself should run on spot instances to save 60-90%.
Practice Questions
What is the difference between Cluster Autoscaler and Karpenter? Answer: Cluster Autoscaler works at the node group level, scaling ASGs up and down. Karpenter works at the instance level, launching the most cost-effective instance type directly — faster scaling, better bin-packing, and native spot prioritization.
How do you find over-provisioned pods in a Kubernetes cluster? Answer: Compare
kubectl top podsusage data against deployment resource requests. Use VPA in recommendation mode to get optimal values, or Kubecost for per-container cost analysis.What is the recommended Strategy for running spot instances with Kubernetes? Answer: Use a dedicated spot node pool with Cluster Autoscaler (or Karpenter with spot as default), set pod disruption budgets on all workloads, use node affinity to prefer spot but allow fallback to on-demand, and run only stateless workloads on spot.
Challenge
Optimize a 50-node EKS cluster: install Kubecost to identify the top 5 most over-provisioned deployments, configure Karpenter with spot instances as default, set VPA in Auto mode for all stateless services, implement namespace quotas for each team, configure Cluster Autoscaler with scale-down after 5 minutes of inactivity, and reduce cluster costs by 40%.
FAQ
What's Next
| Topic | Description |
|---|---|
| {{< card link="../kubernetes-cost-guide" title="Kubernetes Cost Guide" icon="server" >}} | Full K8s cost optimization walkthrough |
| {{< card link="../right-sizing-strategies" title="Right-Sizing Strategies" icon="chart-bar" >}} | Instance right-sizing across clouds |
Related topics: Kubernetes, Cloud Cost Optimization, DevOps
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro