Skip to content

Kubernetes Cost Optimization — Reduce K8s Spending

DodaTech Updated 2026-06-21 6 min read

Kubernetes cost optimization reduces K8s infrastructure spend by right-sizing clusters, setting pod resource limits, using node autoscaling, scheduling on spot instances, and monitoring costs with Kubecost — without sacrificing reliability.

What You'll Learn

By the end of this guide, you'll be able to right-size clusters, implement Cluster Autoscaler and Karpenter, set pod resource requests and limits, use Vertical Pod Autoscaler, adopt spot instances, enforce namespace quotas, monitor costs with Kubecost, and garbage-collect unused resources.

Why It Matters

Kubernetes clusters are notoriously over-provisioned. Teams set generous requests "to be safe," leave nodes running 24/7, and run workloads that could be scheduled on spot instances. The result: 40-60% of K8s spend is waste. DodaTech reduced EKS costs for DodaZIP's backend by 45% using Karpenter spot instances and VPA recommendations.

Real-World Use

Spotify runs 7,000+ Microservices on GKE with 90% spot instance adoption. Pinterest reduced K8s costs by 50% using VPA and Cluster Autoscaler. Snapchat saves $2M+/year by right-sizing pod requests across thousands of services.

flowchart LR
    A[Cluster Metrics] --> B[Right-Size Nodes]
    A --> C[Pod Requests/Limits]
    B --> D[Cluster Autoscaler]
    B --> E[Karpenter]
    C --> F[VPA]
    A --> G[Spot Instances]
    D --> H[30-50% Savings]
    style H fill:#326ce5,color:#fff
â„šī¸ Info

Prerequisites: Kubernetes basics, kubectl access. Understanding of AWS or Azure node types helps.

1. Cluster Right-Sizing

The first step is choosing the right instance type and size for your nodes.

kubectl top nodes

# Check which instance types you're using
kubectl get nodes -o json | jq '.items[].metadata.labels["beta.kubernetes.io/instance-type"]' | sort | uniq -c

Expected output:

NAME           CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
node-m5-2x-1   850m         10%    12Gi            37%
node-m5-2x-2   1200m        15%    18Gi            56%
node-m5-4x-1   900m         5%     20Gi            31%

If all nodes show <50% resource usage, you're over-provisioned. Downsize to smaller instance types.

2. Node Autoscaling

Cluster Autoscaler scales node groups based on pending pods. Karpenter provisions optimal instance types directly.

# Karpenter NodePool (faster, cheaper)
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: "karpenter.sh/capacity-type"
          operator: In
          values: ["on-demand", "spot"]
      nodeClassRef:
        name: default
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenUnderutilized
    expireAfter: 720h
# Cluster Autoscaler (AWS EKS)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cluster-autoscaler
  namespace: kube-system
spec:
  template:
    spec:
      containers:
      - image: k8s.gcr.io/autoscaling/cluster-autoscaler:v1.28.0
        name: cluster-autoscaler
        command:
        - ./cluster-autoscaler
        - --v=4
        - --cloud-provider=aws
        - --scale-down-unneeded-time=10m

3. Pod Resource Requests and Limits

Setting accurate requests and limits is the single highest-impact K8s cost optimization.

# BAD: no requests/limits (unbounded)
apiVersion: v1
kind: Pod
metadata:
  name: web-1
spec:
  containers:
  - name: app
    image: nginx:latest

# GOOD: set requests based on profiling
apiVersion: v1
kind: Pod
metadata:
  name: web-1
spec:
  containers:
  - name: app
    image: nginx:latest
    resources:
      requests:
        cpu: 250m
        memory: 512Mi
      limits:
        cpu: 500m
        memory: 1Gi

Recommendation: Set requests at P99 of observed usage and limits at 2x requests. Use VPA to get these numbers.

4. Vertical Pod Autoscaler (VPA)

VPA analyzes historical pod usage and recommends optimal CPU/memory requests.

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: web-app-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: web-app
  updatePolicy:
    updateMode: "Off"  # Switch to "Auto" after reviewing
  resourcePolicy:
    containerPolicies:
      - containerName: "*"
        minAllowed:
          cpu: 100m
          memory: 128Mi
        maxAllowed:
          cpu: 2
          memory: 4Gi
kubectl describe vpa web-app-vpa
# Expected: Lower: 150m CPU, 256Mi RAM
#           Target: 300m CPU, 512Mi RAM

5. Spot Instances for K8s

Use spot instances for worker nodes running stateless, fault-tolerant workloads:

# EKS managed node group with spot
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: prod
  region: us-east-1
managedNodeGroups:
- name: spot-workers
  instanceTypes:
  - m5.large
  - m5a.large
  spot: true
  minSize: 2
  maxSize: 20
# AKS spot pool
az aks nodepool add \
  --resource-group prod-rg \
  --cluster-name prod-cluster \
  --name spotpool \
  --priority Spot \
  --eviction-policy Delete \
  --node-count 3 \
  --enable-cluster-autoscaler \
  --min-count 1 --max-count 10

6. Namespace Quotas

Prevent one team from consuming all cluster resources:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-backend-quota
  namespace: team-backend
spec:
  hard:
    requests.cpu: "10"
    requests.memory: 40Gi
    limits.cpu: "20"
    limits.memory: 80Gi
    persistentvolumeclaims: 10
    pods: "50"
kubectl describe quota team-backend-quota -n team-backend

7. Cost Monitoring with Kubecost

Kubecost provides per-namespace, per-deployment, and per-label cost breakdowns:

helm repo add kubecost https://kubecost.github.io/cost-analyzer/
helm install kubecost kubecost/cost-analyzer \
  --namespace kubecost \
  --create-namespace

kubectl port-forward --namespace kubecost svc/kubecost-cost-analyzer 9090:9090

8. Garbage Collection

Unused resources accumulate and cost money:

# Clean up completed pods
kubectl delete pods --field-selector=status.phase==Succeeded

# Delete old ReplicaSets with 0 replicas
kubectl delete replicasets --all-namespaces \
  --field-selector=status.replicas==0

Common Mistakes

1. No Resource Requests or Limits

Pods can consume unlimited cluster resources, causing noisy neighbors and unpredictable costs.

2. Ignoring VPA Recommendations

Setting requests by guessing leads to massive over-provisioning. VPA provides data-driven recommendations.

3. Fixed-Size Node Groups

Without Cluster Autoscaler or Karpenter, nodes run 24/7 even when idle. Autoscaling is non-negotiable.

4. No Spot Instances

Stateless workloads (CI/CD, batch, web workers) can run on spot at 60-90% discount. Only databases and stateful services need on-demand.

5. No Namespace Quotas

One team's over-provisioned pods inflate the entire cluster's cost. Quotas enforce fairness.

Practice Questions

1. What is the difference between Cluster Autoscaler and Karpenter? Cluster Autoscaler works with node groups; Karpenter provisions individual optimal instance types. Karpenter achieves higher density and faster scaling.

2. How do requests and limits affect cost? Requests determine the minimum resources reserved for a pod (and billed). Limits cap resource usage. Over-provisioned requests waste money; under-provisioned limits cause throttling.

3. What workloads should not run on spot instances? Stateful workloads (databases), long-running batch jobs without checkpointing, and workloads that cannot tolerate abrupt termination.

4. How does Kubecost help reduce costs? It shows exact cost per namespace, deployment, label, and pod. It identifies idle resources, rightsizing opportunities, and savings from spot adoption.

5. Challenge: Given a 50-node EKS cluster with $25k/month spend: install VPA in recommendation mode for all deployments, implement requests/limits recommendations, switch 70% of nodes to spot using Karpenter, set namespace quotas for all teams, install Kubecost to track savings.

Mini Project: K8s Cost Reporter

class K8sCostReporter:
    def __init__(self):
        self.namespaces = {}

    def add_namespace(self, name, cpu_request, mem_gb, node_cost=0.10):
        cost = (cpu_request * node_cost * 730) + (mem_gb * node_cost * 0.5 * 730)
        self.namespaces[name] = {"cpu": cpu_request, "mem": mem_gb, "cost": round(cost, 2)}

    def report(self):
        total = sum(n["cost"] for n in self.namespaces.values())
        print(f"{'Namespace':<20} {'CPU':<10} {'Memory':<10} {'Monthly Cost':<15}")
        print("-" * 55)
        for name, ns in sorted(self.namespaces.items(), key=lambda x: x[1]["cost"], reverse=True):
            print(f"{name:<20} {ns['cpu']:<10.1f} {ns['mem']:<10.1f} ${ns['cost']:<10.2f}")
        print(f"{'TOTAL':<20} {'':<10} {'':<10} ${total:<10.2f}")

reporter = K8sCostReporter()
reporter.add_namespace("production", 48, 192)
reporter.add_namespace("staging", 12, 48)
reporter.add_namespace("development", 8, 32)
reporter.report()

FAQ

How much can I save on K8s cost optimization?

Most teams save 30-50% in the first quarter through rightsizing, spot adoption, and autoscaling.

Should I use VPA or HPA?

Use VPA for variable resource needs within a pod (recommends CPU/memory). Use HPA for scaling pod count based on load. They complement each other.

What is the biggest waste in K8s?

Over-provisioned pod requests (setting 2 CPU when the app uses 200m). Always use VPA recommendations.

Spot Instances Guide
Multi-Cloud Cost Guide
Cloud Cost Tools

What's Next

You now understand Kubernetes cost optimization! Next, explore Multi-Cloud Cost optimization for managing costs across K8s clusters on AWS, Azure, and GCP.

  • Practice daily — Review Kubecost dashboard and identify top spenders
  • Build a project — Automate VPA recommendations roll-out across namespaces
  • Explore related topics — Check out Karpenter vs Cluster Autoscaler comparison

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro