Skip to content
Codeloom
DevOps

Kubernetes Autoscaling with HPA, VPA, and KEDA

Master Kubernetes autoscaling using HPA for horizontal scaling, VPA for right-sizing, and KEDA for event-driven workloads.

·6 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How HPA scales pods based on CPU and custom metrics
  • How VPA right-sizes container resource requests
  • When to use KEDA for event-driven autoscaling
  • Practical YAML configurations for each approach
  • Common pitfalls and how to avoid them

Prerequisites

None — this post is self-contained.

Kubernetes does not automatically scale your workloads. It keeps the number of pods you asked for, but it does not know whether that number is right. Autoscaling bridges the gap between the capacity you provisioned and the capacity you actually need. Kubernetes offers three complementary autoscaling mechanisms, and understanding when to use each one is the difference between wasted resources and a system that adapts to demand.

Horizontal Pod Autoscaler (HPA)

HPA adds or removes pod replicas based on observed metrics. The most common trigger is CPU utilization, but HPA v2 supports memory, custom metrics, and external metrics.

Basic CPU-Based HPA

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

This configuration scales the api Deployment between 2 and 20 replicas, adding pods when average CPU utilization exceeds 70 percent and removing them when it drops below.

HPA requires that your pods have CPU resource requests defined. Without requests, HPA cannot calculate utilization percentages:

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi

Custom Metrics HPA

CPU is a lagging indicator. By the time CPU spikes, requests are already queuing. A better signal is often the application-level metric like requests per second or queue depth. Using the Prometheus adapter:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: 100
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60

The behavior block controls scaling speed. Scale up quickly (double capacity within 60 seconds) but scale down slowly (10 percent per minute with a 5-minute stabilization window). This prevents flapping during traffic fluctuations.

Vertical Pod Autoscaler (VPA)

VPA adjusts the CPU and memory requests of your containers rather than changing the number of replicas. It solves a different problem: right-sizing. Most teams guess at resource requests and either waste capacity with over-provisioning or cause OOMKills with under-provisioning.

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  updatePolicy:
    updateMode: Auto
  resourcePolicy:
    containerPolicies:
      - containerName: api
        minAllowed:
          cpu: 100m
          memory: 128Mi
        maxAllowed:
          cpu: 2
          memory: 2Gi

VPA observes actual resource usage over time and adjusts requests accordingly. In Auto mode, it evicts pods and recreates them with updated requests. In Off mode, it only provides recommendations without making changes, which is useful when you want to review suggestions before applying them.

VPA and HPA Together

Running VPA and HPA on the same metric (CPU) causes conflicts. VPA increases CPU requests, which changes the utilization percentage HPA uses, leading to unpredictable behavior.

The safe combination is HPA scaling on a custom metric (like requests per second) while VPA handles CPU and memory right-sizing. Alternatively, use VPA in recommendation-only mode and apply its suggestions manually during periodic reviews.

KEDA: Event-Driven Autoscaling

KEDA (Kubernetes Event-Driven Autoscaler) extends HPA with event-driven triggers. While HPA polls metrics on a fixed interval, KEDA can scale based on queue lengths, stream lag, cron schedules, and dozens of other event sources.

Scaling Based on Queue Depth

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: order-processor
spec:
  scaleTargetRef:
    name: order-processor
  minReplicaCount: 0
  maxReplicaCount: 50
  triggers:
    - type: rabbitmq
      metadata:
        host: amqp://rabbitmq.default.svc:5672
        queueName: orders
        queueLength: "10"

This scales the order-processor Deployment based on the RabbitMQ queue length. When the queue has 100 messages and the target is 10 messages per replica, KEDA requests 10 replicas. When the queue is empty, it scales to zero.

Scale to Zero

KEDA’s signature feature is scaling to zero. Standard HPA requires at least one replica. KEDA can scale a Deployment to zero replicas when there is no work, saving resources for batch processors, event handlers, and other bursty workloads:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: report-generator
spec:
  scaleTargetRef:
    name: report-generator
  minReplicaCount: 0
  maxReplicaCount: 10
  cooldownPeriod: 300
  triggers:
    - type: cron
      metadata:
        timezone: America/New_York
        start: 0 9 * * 1-5
        end: 0 18 * * 1-5
        desiredReplicas: "3"
    - type: prometheus
      metadata:
        serverAddress: http://prometheus:9090
        metricName: pending_reports
        query: pending_reports_total
        threshold: "5"

This example combines a cron trigger (scale up during business hours) with a Prometheus metric trigger (scale based on pending reports).

Choosing the Right Approach

ScenarioAutoscaler
Web API with variable trafficHPA on CPU or request rate
Batch job processing a queueKEDA with queue trigger
Right-sizing over-provisioned podsVPA in recommendation mode
Event-driven functions that idle oftenKEDA with scale-to-zero
Stateless service with predictable patternsHPA with cron-based min replicas

Common Pitfalls

Missing resource requests. HPA cannot calculate CPU utilization without requests. Always define them.

Scaling on memory for leak-prone apps. If your application has a memory leak, HPA will scale out indefinitely because memory never decreases. Fix the leak; do not autoscale around it.

Too-aggressive scale-down. Scaling down fast causes request failures during traffic oscillations. Use stabilization windows of at least 5 minutes for scale-down.

Ignoring pod startup time. If your pod takes 60 seconds to start and your traffic spike lasts 30 seconds, autoscaling will not help. Optimize startup time or use proactive scaling based on leading indicators.

Wrap-up

HPA handles the common case of scaling replicas based on load. VPA right-sizes individual containers so you stop guessing at resource requests. KEDA adds event-driven triggers and scale-to-zero for bursty workloads. Use them together, carefully, and your cluster will adapt to demand instead of burning money on idle pods.