Back to Blog

Kubernetes Cost Optimization: VPA, HPA, Bin-Packing, Spot Nodes

Cut Kubernetes costs by 40–60%: configure Vertical Pod Autoscaler for right-sizing, Horizontal Pod Autoscaler for traffic-based scaling, bin-packing with pod topology, spot node groups with Karpenter, and idle resource cleanup.

Viprasol Tech Team
13 min read
Updated 2026

Most Kubernetes clusters are significantly over-provisioned. Developers set resource requests conservatively (or copy from StackOverflow), nodes run at 20–30% utilization, and the bill accumulates quietly. A systematic cost optimization pass typically finds 40–60% savings without touching application performance.

The optimization hierarchy: right-size pods first (VPA), scale to demand (HPA/KEDA), pack pods efficiently (topology), then use spot for everything that tolerates interruption (Karpenter).


Step 1: Right-Sizing with VPA

Vertical Pod Autoscaler observes actual CPU and memory usage, then recommends (or automatically adjusts) resource requests:

# kubernetes/vpa/recommendation-mode.yaml

> **Quick answer.** Kubernetes clusters often run at 20-30% utilization, and a systematic optimization pass typically recovers 40-60% of spend without hurting performance. Follow the hierarchy: right-size pod requests with VPA, scale to demand with HPA/KEDA, pack pods efficiently via topology, then move interruption-tolerant workloads to spot nodes with Karpenter.
# Start in recommendation mode — don't auto-apply yet
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-server-vpa
  namespace: production
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  updatePolicy:
    updateMode: "Off"  # Recommendation only — read with: kubectl get vpa api-server-vpa
  resourcePolicy:
    containerPolicies:
    - containerName: api
      # Set bounds to prevent VPA from recommending unreasonable values
      minAllowed:
        cpu: 50m
        memory: 128Mi
      maxAllowed:
        cpu: 2000m
        memory: 4Gi
      controlledResources: ["cpu", "memory"]
# Read VPA recommendations after 24-48 hours of traffic
kubectl describe vpa api-server-vpa -n production

# Output (relevant section):
# Recommendation:
#   Container Recommendations:
#     Container Name: api
#     Lower Bound:
#       CPU:     80m
#       Memory:  256Mi
#     Target:               ← Use these values for your resource requests
#       CPU:     150m
#       Memory:  512Mi
#     Upper Bound:
#       CPU:     800m
#       Memory:  1.5Gi
#     Uncapped Target:
#       CPU:     150m
#       Memory:  512Mi
# Apply VPA recommendations to your deployment
# kubernetes/deployments/api-server.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  template:
    spec:
      containers:
      - name: api
        resources:
          requests:
            cpu: "150m"      # VPA recommended target
            memory: "512Mi"  # VPA recommended target
          limits:
            cpu: "800m"      # VPA upper bound × 1.2 buffer
            memory: "1.5Gi" # Match memory limit to upper bound (OOM prevention)

VPA in Auto mode — only enable after validating recommendations:

spec:
  updatePolicy:
    updateMode: "Auto"  # VPA will evict and restart pods to apply new requests
    # Note: causes brief restarts — only use with proper PodDisruptionBudget

Step 2: Horizontal Scaling with HPA

HPA scales pod count based on metrics. Don't just use CPU — scale on what actually drives load:

# kubernetes/hpa/api-server-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 2    # Never go below 2 for HA
  maxReplicas: 20
  metrics:
  # Scale on CPU utilization
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70  # Target 70% CPU — leaves headroom for spikes

  # Scale on custom metric: requests per second per pod
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "100"  # Scale when > 100 RPS per pod

  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60  # Wait 60s before scaling up again
      policies:
      - type: Pods
        value: 4            # Add at most 4 pods at a time
        periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300  # Wait 5 min before scaling down
      policies:
      - type: Percent
        value: 25           # Remove at most 25% of pods at a time
        periodSeconds: 60

KEDA for Event-Driven Scaling

# kubernetes/keda/queue-scaler.yaml
# Scale workers based on SQS queue depth
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: worker-scaler
  namespace: production
spec:
  scaleTargetRef:
    name: background-worker
  minReplicaCount: 0  # Scale to zero when queue is empty (save cost)
  maxReplicaCount: 50
  pollingInterval: 15  # Check queue every 15 seconds
  cooldownPeriod: 300  # Wait 5 min after last message before scaling to 0
  triggers:
  - type: aws-sqs-queue
    authenticationRef:
      name: keda-aws-credentials
    metadata:
      queueURL: "https://sqs.us-east-1.amazonaws.com/123456789/jobs-queue"
      queueLength: "10"   # Target: 10 messages per worker replica
      awsRegion: "us-east-1"
      scaleOnInFlight: "true"

☁️ Is Your Cloud Costing Too Much?

Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.

  • AWS, GCP, Azure certified engineers
  • Infrastructure as Code (Terraform, CDK)
  • Docker, Kubernetes, GitHub Actions CI/CD
  • Typical audit recovers $500–$3,000/month in savings

Step 3: Bin-Packing Pod Topology

Kubernetes default scheduler spreads pods across nodes. For cost, you want the opposite — pack pods densely so fewer nodes are needed:

# kubernetes/deployments/api-server.yaml
spec:
  template:
    spec:
      # Topology spread — prefer same node, allow spreading
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: ScheduleAnyway  # Don't block scheduling if can't satisfy
        labelSelector:
          matchLabels:
            app: api-server

      # Pod Anti-Affinity: keep critical replicas on different nodes (HA)
      # Use for databases, stateful services
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              topologyKey: kubernetes.io/hostname
              labelSelector:
                matchLabels:
                  app: api-server
                  tier: critical
# Cluster-level bin-packing with Descheduler
# Periodically evicts pods from under-utilized nodes
apiVersion: "descheduler/v1alpha2"
kind: "DeschedulerPolicy"
profiles:
- name: default
  pluginConfig:
  - name: LowNodeUtilization
    args:
      thresholds:
        cpu: 20     # Node is "low" if CPU < 20%
        memory: 20
        pods: 20
      targetThresholds:
        cpu: 50     # Move pods to nodes with CPU < 50%
        memory: 50
        pods: 100
  plugins:
    balance:
      enabled:
      - LowNodeUtilization
    deschedule:
      enabled:
      - RemovePodsViolatingTopologySpreadConstraint

Step 4: Spot Instances with Karpenter

Karpenter provisions nodes just-in-time and prefers spot instances when workloads allow:

# kubernetes/karpenter/node-pool.yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: general-purpose
spec:
  template:
    metadata:
      labels:
        node-type: general-purpose
    spec:
      nodeClassRef:
        apiVersion: karpenter.k8s.aws/v1
        kind: EC2NodeClass
        name: default
      requirements:
      # Allow multiple instance families for better spot availability
      - key: karpenter.k8s.aws/instance-family
        operator: In
        values: ["m5", "m5a", "m6i", "m6a", "m7i", "m7a"]
      - key: karpenter.k8s.aws/instance-size
        operator: In
        values: ["large", "xlarge", "2xlarge"]
      # Mix spot and on-demand
      - key: karpenter.sh/capacity-type
        operator: In
        values: ["spot", "on-demand"]
      - key: kubernetes.io/arch
        operator: In
        values: ["amd64"]
  disruption:
    consolidationPolicy: WhenUnderutilized  # Remove nodes when not needed
    consolidateAfter: 30s
  limits:
    cpu: 1000        # Max 1000 vCPUs across this pool
    memory: 4000Gi
# kubernetes/karpenter/ec2-node-class.yaml
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2023
  role: "KarpenterNodeRole-production"  # IAM role for nodes
  subnetSelectorTerms:
  - tags:
      karpenter.sh/discovery: production
  securityGroupSelectorTerms:
  - tags:
      karpenter.sh/discovery: production
  blockDeviceMappings:
  - deviceName: /dev/xvda
    ebs:
      volumeSize: 50Gi
      volumeType: gp3
      iops: 3000
      encrypted: true
  tags:
    Environment: production

Spot Interruption Handling

# kubernetes/spot-handler/deployment.yaml
# AWS Node Termination Handler — gracefully drain spot nodes before interruption
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: aws-node-termination-handler
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: aws-node-termination-handler
  template:
    spec:
      containers:
      - name: aws-node-termination-handler
        image: public.ecr.aws/aws-ec2/aws-node-termination-handler:v1.22.0
        env:
        - name: ENABLE_SPOT_INTERRUPTION_DRAINING
          value: "true"
        - name: NODE_TERMINATION_GRACE_PERIOD
          value: "120"  # 2 minutes to drain
        - name: POD_TERMINATION_GRACE_PERIOD
          value: "60"   # 60s for pods to shut down
        - name: ENABLE_REBALANCE_MONITORING
          value: "true" # Proactive rebalancing before interruption

kubernetes - Kubernetes Cost Optimization: VPA, HPA, Bin-Packing, Spot Nodes

⚙️ DevOps Done Right — Zero Downtime, Full Automation

Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.

  • Staging + production environments with feature flags
  • Automated security scanning in the pipeline
  • Uptime monitoring + alerting + runbook automation
  • On-call support handover docs included

Cost Savings Comparison

OptimizationTypical SavingsEffortRisk
VPA right-sizing20–40%MediumLow (recommendation mode first)
HPA (remove over-provisioning)15–30%LowLow
KEDA scale-to-zero for workers50–80% on workersLowLow
Spot instances for workers60–70% vs on-demandMediumMedium (interruption handling)
Karpenter bin-packing20–35% on node countMediumLow
Spot for stateless app pods50–65%MediumMedium
Combined40–65% total

Namespace Resource Quotas (Guardrails)

# kubernetes/quotas/production-namespace.yaml
# Prevent any team from accidentally provisioning unlimited resources
apiVersion: v1
kind: ResourceQuota
metadata:
  name: production-quota
  namespace: production
spec:
  hard:
    requests.cpu: "100"       # Total CPU requests across all pods
    requests.memory: 200Gi
    limits.cpu: "200"
    limits.memory: 400Gi
    pods: "500"               # Max pod count
    count/deployments.apps: "50"

---
# LimitRange: defaults for containers that don't specify resources
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: production
spec:
  limits:
  - default:
      cpu: "500m"
      memory: "512Mi"
    defaultRequest:
      cpu: "100m"
      memory: "128Mi"
    type: Container

Explore More


What Viprasol Offers

Kubernetes cost optimization is systematic engineering work — profiling actual resource usage, implementing autoscaling policies, migrating workloads to spot, and maintaining the guardrails that prevent costs from drifting back. Our platform engineers typically achieve 40–60% cost reduction within 4–6 weeks of engagement.

Cloud engineering services → | Talk to our engineers →

Full-Stack Kubernetes Cost Optimization: Bin-Packing, Pod Rightsizing, and Autoscaling Done Right

Real kubernetes cost optimization comes from treating requests, limits, and scheduling as one connected system rather than separate dials. We start with kubernetes resource optimization: measuring actual CPU and memory usage, then applying pod rightsizing so requests reflect reality instead of guesswork. Tight, accurate requests unlock effective bin packing in kubernetes, letting the scheduler place more workloads per node and shrink idle headroom. From there, the horizontal pod autoscaler scales replicas to live demand, while VPA refines per-pod resources over time and spot nodes absorb fault-tolerant batch work cheaply. Unlike single-purpose pod rightsizing platforms, our senior engineers take full ownership of the whole loop, delivering full-stack kubernetes optimization that ties metrics, autoscaling policy, and node strategy together so savings hold up under production load.

kubernetesdevopsawscost-optimizationterraform
Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need DevOps & Cloud Expertise?

Scale your infrastructure with confidence. AWS, GCP, Azure certified team.

Free consultation • No commitment • Response within 24 hours

Viprasol · Big Data & Analytics

Making sense of your data at scale?

Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.