Skip to content

Kubernetes Cost Optimization: Reduce K8s Spending

DodaTech Updated 2026-06-20 7 min read

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

  1. 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.

  2. No cluster autoscaler: Fixed-size node pools run empty nodes at full cost. Always configure Cluster Autoscaler or Karpenter.

  3. 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.

  4. No namespace quotas: Without quotas, one team can consume the entire cluster. Set ResourceQuota and LimitRange per namespace.

  5. Running monitoring infrastructure on on-demand nodes: Prometheus, Grafana, and Kubecost itself should run on spot instances to save 60-90%.

Practice Questions

  1. 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.

  2. How do you find over-provisioned pods in a Kubernetes cluster? Answer: Compare kubectl top pods usage data against deployment resource requests. Use VPA in recommendation mode to get optimal values, or Kubecost for per-container cost analysis.

  3. 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

Should I use VPA or HPA?

: Use both. VPA right-sizes the pod (vertical scaling), HPA adjusts the replica count (horizontal scaling). VPA works best with HPA when the workload has variable load.

How much can I save by migrating to spot instances in K8s?

: 60-90% on compute costs for spot-eligible workloads. Most web applications, batch jobs, and CI/CD pipelines run well on spot. Use pod disruption budgets to handle interruptions.

What is bin-packing in Kubernetes?

: Bin-packing means scheduling pods onto the fewest possible nodes by optimizing resource allocation. Karpenter does this automatically; you can also use tools like Descheduler for existing clusters.

How do I track Kubernetes costs by team?

: Use Kubecost with team-based namespace labels. Allocate costs by namespace and set budgets per team. Export to CSV for chargeback reporting.

Does Karpenter work on Azure and GCP?

: Yes — Karpenter is cloud-agnostic. It supports AKS (Azure) and GKE (GCP) in addition to EKS. The Provisioner configuration varies by cloud provider.

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