Horizontal Pod Autoscaling in Kubernetes

Traffic to your applications is rarely constant. A news site gets a surge when a story breaks. An e-commerce app spikes during a sale. Horizontal Pod Autoscaling (HPA) lets Kubernetes grow or shrink your deployment’s replica count automatically in response to real demand, so you pay for capacity when you need it and not when you do not.

How the HPA controller works

The HPA controller runs inside Kubernetes and watches the pods belonging to a target Deployment (or StatefulSet or other scalable resource). It periodically reads metrics — CPU utilization, memory usage, or custom metrics — and calculates the desired replica count using this formula:

desiredReplicas = ceil(currentReplicas × (currentMetricValue / targetMetricValue))

If your target is 50% CPU and your pods are currently running at 90% CPU, HPA calculates: ceil(3 × (90 / 50)) = ceil(5.4) = 6. It then scales up to 6 replicas.

When CPU drops below the target, HPA scales back down — but slowly, with a 5-minute cooldown by default to avoid constant scale-up/scale-down oscillation.

Prerequisites: metrics-server

HPA reads metrics from the Kubernetes metrics API, which is served by the metrics-server component. Verify it is running:

# Check if metrics-server is installed
kubectl get deployment metrics-server -n kube-system

# If missing, install it (for AKS with monitoring add-on, it's usually present)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# Verify metrics are available (takes 1-2 minutes after install)
kubectl top nodes
kubectl top pods

Creating an HPA — CPU-based scaling

First, ensure the target Deployment has CPU resource requests defined. HPA uses the request as the baseline for percentage calculations — without a request, CPU-based HPA does not function.

# deployment with resource requests
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: web-app
        image: nginx:1.25
        resources:
          requests:
            cpu: "200m"
            memory: "256Mi"
          limits:
            cpu: "500m"
            memory: "512Mi"
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
kubectl apply -f hpa.yaml

# Check HPA status
kubectl get hpa web-app-hpa
# NAME          REFERENCE          TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
# web-app-hpa   Deployment/web-app 12%/60%   2         20        2          5m

The TARGETS column shows current/target. When current usage exceeds 60%, HPA adds replicas. Below target, it scales down (respecting the minimum).

Memory-based scaling

You can scale on memory utilization too, or combine CPU and memory:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: cache-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: cache-app
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 75

When multiple metrics are specified, HPA calculates the desired replicas for each metric independently and uses the highest value. If CPU says scale to 5 and memory says scale to 8, it scales to 8.

Note

Memory-based autoscaling can be problematic. Memory usage often does not decrease when load drops because applications cache data in memory. This can prevent HPA from scaling down. Use memory scaling only when you have a clear correlation between load and memory usage.

Scaling on custom metrics

Business metrics often matter more than CPU. Scale on queue depth, request count, or any metric exposed by your application or Azure services. This requires the Azure Kubernetes Metrics Adapter or KEDA.

KEDA (Kubernetes Event-Driven Autoscaling) is built into AKS and provides the richest set of scalers. Enable it:

# Enable KEDA on an AKS cluster
az aks update \
  --resource-group my-aks-rg \
  --name my-cluster \
  --enable-keda

KEDA ScaledObject for Azure Service Bus queue depth:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: order-processor-scaler
spec:
  scaleTargetRef:
    name: order-processor
  minReplicaCount: 0
  maxReplicaCount: 30
  triggers:
  - type: azure-servicebus
    metadata:
      queueName: orders
      namespace: my-servicebus-namespace
      messageCount: "5"   # Scale up when >5 messages per replica
    authenticationRef:
      name: servicebus-auth

This scales the order-processor Deployment to 0 when the queue is empty (saving cost completely) and up to 30 replicas at 5 messages per replica when it fills up.

Testing HPA with a load generator

Verify your HPA works by generating artificial load:

# Deploy a load generator
kubectl run load-generator \
  --image=busybox \
  --restart=Never \
  -- /bin/sh -c "while true; do wget -q -O- http://web-app; done"

# In another terminal, watch the HPA respond
kubectl get hpa web-app-hpa --watch

# And watch pods scale up
kubectl get pods -l app=web-app --watch

# Stop the load generator when done
kubectl delete pod load-generator

# Watch pods scale back down (takes ~5 minutes)
kubectl get hpa web-app-hpa --watch

Tuning scale-up and scale-down behavior

The default HPA behavior is conservative about scaling down. You can tune this with the behavior field:

spec:
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30    # Wait 30s before scaling up
      policies:
      - type: Pods
        value: 4                         # Add at most 4 pods per scale step
        periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300   # Wait 5 min before scaling down
      policies:
      - type: Pods
        value: 2                         # Remove at most 2 pods per scale step
        periodSeconds: 60
      - type: Percent
        value: 10                        # Or 10% of current replicas
        periodSeconds: 60
      selectPolicy: Min                  # Use the more conservative policy

Aggressive scale-up and conservative scale-down is the right approach for most services. Scaling up quickly handles traffic spikes. Scaling down slowly avoids thrashing if traffic is fluctuating around the threshold.

What HPA cannot do

HPA has real limitations to be aware of:

  • It cannot scale below minReplicas. Setting minReplicas to 0 is technically possible in autoscaling/v2 but requires KEDA for the 0-to-N scaling to work well.
  • It does not provision nodes. If HPA wants to add pods but all nodes are full, pods stay Pending until the Cluster Autoscaler adds a node. Plan for this latency.
  • It works poorly for very short spikes. A 15-second traffic burst is over before HPA even evaluates metrics. For flash traffic, consider pre-scaling or using caching at the edge.
  • It conflicts with manual scaling. If you manually scale a Deployment that has an HPA, the HPA will override your change on the next evaluation cycle. Let the HPA control the replica count.

Common mistakes

  1. No resource requests on containers. HPA cannot calculate CPU utilization percentage without a request value to compare against. CPU-based HPA silently does nothing if requests are not set.
  2. Setting maxReplicas too low. If your cluster can handle 50 pods but maxReplicas is 5, HPA cannot scale to handle large traffic spikes. Set maxReplicas based on what your infrastructure and budget can support at peak load.
  3. Manually scaling a Deployment with an HPA. The HPA controller owns the replica count. It overwrites manual changes. If you want to override HPA temporarily, delete it first, scale manually, then recreate it.
  4. Setting the target utilization too high. A target of 90% CPU sounds efficient but leaves no headroom. When a traffic spike hits, the existing pods hit 90% before HPA can scale up and add capacity. 60-70% is a safer target for latency-sensitive services.
  5. Not accounting for node provisioning time. HPA can request 20 more pods in seconds, but if nodes need to be added first, those pods sit Pending for 3-5 minutes. Pair HPA with the Cluster Autoscaler and keep enough spare node capacity for expected scale-up.

Frequently asked questions

What is the difference between HPA and Cluster Autoscaler?

HPA scales the number of pods within a Deployment. Cluster Autoscaler scales the number of nodes in a node pool. They work together: HPA adds pods, and if there are not enough nodes to schedule them, Cluster Autoscaler adds nodes.

How quickly does HPA respond to traffic spikes?

By default, HPA checks metrics every 15 seconds and scales up within 30-60 seconds of detecting sustained high usage. Scaling down is more conservative — it waits 5 minutes by default to avoid thrashing.

Do I need to install anything for HPA to work?

The metrics-server must be running in your cluster. AKS installs it automatically when you enable the monitoring add-on. Without metrics-server, HPA cannot read CPU and memory metrics and stays at its current replica count.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.