← Back to DevBytes

Horizontal Pod Autoscaler: Complete Implementation Guide

Introduction to the Horizontal Pod Autoscaler

The Horizontal Pod Autoscaler (HPA) is a core Kubernetes controller that automatically scales the number of pods in a deployment, replication controller, replica set, or stateful set based on observed resource utilization or custom metrics. Instead of manually adding or removing pods when traffic spikes or CPU climbs, HPA handles this dynamically, ensuring your application maintains optimal performance without human intervention.

What Exactly Is the Horizontal Pod Autoscaler?

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

At its heart, HPA is a control loop that periodically queries metrics from the Metrics API (or custom/external metrics APIs), compares the observed values against user-defined target thresholds, and adjusts the replica count accordingly. It scales horizontally—meaning it adds more instances (pods) rather than increasing the resources of a single instance (which would be vertical scaling).

The HPA controller runs as part of the kube-controller-manager and follows a simple control loop logic:

Key Terminology

Why the Horizontal Pod Autoscaler Matters

In production environments, traffic patterns are unpredictable. A service might sit idle at 3 AM and then face a tenfold traffic surge during business hours. Without autoscaling, you have two poor options:

HPA solves this by maintaining a sweet spot—you define the desired performance target, and HPA adjusts the pod count to meet that target continuously. This means:

Prerequisites: The Metrics Server

HPA relies on a metrics source to obtain resource utilization data. For CPU and memory metrics, you must install the Metrics Server in your cluster. Without it, HPA cannot fetch pod resource metrics and will report errors like:

kubectl describe hpa my-app-hpa
# Output includes:
# Warning  FailedGetResourceMetric  horizontal-pod-autoscaler  unable to get metrics for resource cpu: no metrics returned from resource metrics API

Install the Metrics Server in a standard Kubernetes cluster:

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

# If not present, install it via the official manifest (check version compatibility)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# Verify it's running and serving metrics
kubectl top pods -n kube-system
kubectl top nodes

Important note for minikube users: Enable the metrics-server addon:

minikube addons enable metrics-server

For clusters with self-signed certificates or non-standard configurations, you may need to patch the metrics-server deployment with --kubelet-insecure-tls:

kubectl patch deployment metrics-server -n kube-system \
  --type='json' -p='[{"op":"add", "path":"/spec/template/spec/containers/0/args/-", "value":"--kubelet-insecure-tls"}]'

Creating Your First Horizontal Pod Autoscaler

You can create an HPA using either the imperative kubectl autoscale command or a declarative YAML manifest. Let's walk through both approaches with a complete example.

Step 1: Deploy a Sample Application

First, create a Deployment with defined resource requests—this is crucial because HPA needs a baseline to calculate utilization percentages.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-apache
  labels:
    app: php-apache
spec:
  replicas: 1
  selector:
    matchLabels:
      app: php-apache
  template:
    metadata:
      labels:
        app: php-apache
    spec:
      containers:
      - name: php-apache
        image: k8s.gcr.io/hpa-example
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: "200m"
            memory: "256Mi"
          limits:
            cpu: "500m"
            memory: "512Mi"
---
apiVersion: v1
kind: Service
metadata:
  name: php-apache-service
spec:
  selector:
    app: php-apache
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP

Apply this to your cluster:

kubectl apply -f deployment.yaml

Step 2: Create the HPA Imperatively

The fastest way to enable autoscaling:

kubectl autoscale deployment php-apache \
  --min=1 \
  --max=10 \
  --cpu-percent=50

This creates an HPA that maintains ~50% CPU utilization across all pods, scaling between 1 and 10 replicas.

Step 3: Create the HPA Declaratively (Recommended for Production)

A declarative YAML manifest gives you full control and allows version tracking in Git:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50

Apply and verify:

kubectl apply -f hpa.yaml
kubectl get hpa php-apache-hpa -w

Step 4: Generate Load to Observe Scaling

In a separate terminal, run a temporary pod that sends continuous requests:

kubectl run -i --tty load-generator --rm --image=busybox:1.28 --restart=Never -- /bin/sh -c \
"while true; do wget -q -O- http://php-apache-service.default.svc.cluster.local; done"

Watch the HPA react:

kubectl get hpa php-apache-hpa --watch

# You'll see output similar to:
# NAME             REFERENCE               TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
# php-apache-hpa   Deployment/php-apache   0%/50%    1         10        1          2m
# php-apache-hpa   Deployment/php-apache   45%/50%   1         10        2          3m
# php-apache-hpa   Deployment/php-apache   98%/50%   1         10        4          4m
# php-apache-hpa   Deployment/php-apache   52%/50%   1         10        8          5m

Within a couple of minutes, the pod count climbs as CPU load exceeds the 50% target. Stop the load generator and watch the pods gradually scale back down after the cool-down period.

Understanding the autoscaling/v2 API Metrics

The autoscaling/v2 API (stable since Kubernetes 1.23) supports four metric types, giving you tremendous flexibility:

1. Resource Metrics

Resource metrics measure CPU or memory utilization across pods. This is the most common and straightforward type:

metrics:
- type: Resource
  resource:
    name: cpu
    target:
      type: Utilization
      averageUtilization: 60
- type: Resource
  resource:
    name: memory
    target:
      type: Utilization
      averageUtilization: 70

You can also use AverageValue instead of Utilization to specify an absolute value rather than a percentage of the pod's request:

metrics:
- type: Resource
  resource:
    name: cpu
    target:
      type: AverageValue
      averageValue: "100m"

2. Pod Metrics (Custom Metrics)

Pod metrics allow scaling based on metrics specific to your application, such as requests per second, queue length, or active connections. These require a metrics provider like Prometheus Adapter:

metrics:
- type: Pods
  pods:
    metric:
      name: http_requests_per_second
    target:
      type: AverageValue
      averageValue: "100"

3. Object Metrics (Custom Metrics)

Object metrics scale based on a metric attached to a Kubernetes object other than pods—for example, scaling based on the message count of a specific RabbitMQ queue object:

metrics:
- type: Object
  object:
    metric:
      name: rabbitmq_queue_messages
    describedObject:
      apiVersion: v1
      kind: Service
      name: rabbitmq-service
    target:
      type: Value
      value: "500"

4. External Metrics

External metrics allow scaling based on metrics from outside the cluster, such as Cloud Pub/Sub subscription backlog, AWS SQS queue depth, or third-party monitoring services:

metrics:
- type: External
  external:
    metric:
      name: pubsub.googleapis.com|subscription|num_undelivered_messages
      selector:
        matchLabels:
          subscription: my-subscription
    target:
      type: AverageValue
      averageValue: "100"

For external metrics, you'll need a metrics adapter like Stackdriver Metrics Adapter (GCP), CloudWatch Metrics Adapter (AWS), or a Prometheus Adapter configured to scrape external sources.

Advanced Scaling Behavior Configuration

Starting with autoscaling/v2 (stable in 1.23), you can fine-tune scaling behavior to prevent flapping, control scale-up/scale-down speeds, and handle bursty workloads gracefully. The behavior field lets you define policies for both scale-up and scale-down.

Complete Example with Custom Scaling Behavior

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: production-app-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: production-app
  minReplicas: 3
  maxReplicas: 50
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 65
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15
      - type: Pods
        value: 10
        periodSeconds: 15
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
      - type: Pods
        value: 2
        periodSeconds: 120
      selectPolicy: Min

Understanding Stabilization Windows

The stabilizationWindowSeconds parameter prevents rapid fluctuations by considering the highest desired replica count during the window for scale-up, and the lowest desired replica count for scale-down. This is essentially a "cool-down" period:

A scale-down window of 300 seconds (5 minutes) is common and prevents thrashing. A scale-up window of 0 seconds means "react immediately to spikes."

Policy Types and SelectPolicy

You can define multiple policies per direction. The selectPolicy determines which policy's computed replica change wins:

Policy Types Explained

In the example above, during scale-up, the HPA can either double the replica count or add 10 pods within 15 seconds—whichever is larger (Max). During scale-down, it reduces by either 10% or 2 pods within their respective periods—whichever is smaller (Min).

Multiple Metrics and Complex Scenarios

When you specify multiple metrics, HPA calculates the desired replica count for each metric independently and then picks the largest value. This ensures that all constraints are satisfied. For example, if CPU load demands 8 replicas but memory demands only 3 replicas, HPA scales to 8 to satisfy both conditions.

# Example: Both CPU and memory must stay under target
metrics:
- type: Resource
  resource:
    name: cpu
    target:
      type: Utilization
      averageUtilization: 50
- type: Resource
  resource:
    name: memory
    target:
      type: Utilization
      averageUtilization: 70
# If CPU calculation says 10 replicas and memory says 6, HPA picks 10.

You can also combine resource metrics with custom metrics for sophisticated scaling logic:

metrics:
- type: Resource
  resource:
    name: cpu
    target:
      type: Utilization
      averageUtilization: 60
- type: Pods
  pods:
    metric:
      name: http_request_rate
    target:
      type: AverageValue
      averageValue: "200"
# The larger of the two calculations wins.

Troubleshooting HPA: Common Issues and Solutions

HPA Shows <unknown> in TARGETS Column

This almost always means the metrics server isn't running or isn't reachable. Verify:

# Check if metrics-server pods are running
kubectl get pods -n kube-system | grep metrics-server

# Check if kubectl top works
kubectl top pods -n your-namespace

# Check HPA events
kubectl describe hpa your-hpa-name | grep -A10 Events

Common fixes include reinstalling metrics-server, checking API service registration, and ensuring network policies allow traffic to the metrics-server.

HPA Never Scales Up Despite High Load

HPA Scales Too Aggressively (Flapping)

Custom Metrics Not Working

Custom and external metrics require a properly configured metrics adapter. For Prometheus-based setups:

# Check if custom metrics API is registered
kubectl get apiservices | grep custom.metrics

# Verify the adapter is running
kubectl get pods -n monitoring | grep prometheus-adapter

# Check available custom metrics
kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1/ | jq .

Ensure your Prometheus instance is scraping the metrics and the adapter configuration maps the correct metric names.

Best Practices for Production HPA Deployments

1. Always Set Resource Requests

Resource requests are the denominator in utilization calculations. Without them, HPA cannot compute percentages. Set realistic requests based on observed baseline usage:

resources:
  requests:
    cpu: "250m"
    memory: "512Mi"
  limits:
    cpu: "1"
    memory: "1Gi"

2. Configure Stabilization Windows Thoughtfully

Avoid the default behavior (which has a 5-minute scale-down stabilization window but no scale-up stabilization). For production, explicitly set both:

3. Use Multiple Metrics for Robustness

Don't rely on a single metric. Combine CPU with a custom metric like request latency or queue depth to capture different dimensions of load:

metrics:
- type: Resource
  resource:
    name: cpu
    target:
      type: Utilization
      averageUtilization: 70
- type: Pods
  pods:
    metric:
      name: average_request_latency_ms
    target:
      type: AverageValue
      averageValue: "200"

4. Set Appropriate Min Replicas

The minReplicas value should reflect your baseline redundancy needs, not zero. A minimum of 2 or 3 pods ensures high availability even during zero-traffic periods and provides a buffer for sudden spikes while HPA reacts:

minReplicas: 3  # Not 1, unless you're in a dev environment

5. Avoid Overlapping with Other Scaling Mechanisms

If you use KEDA (Kubernetes Event-Driven Autoscaling) or a custom operator that also modifies replica counts, ensure they don't conflict with HPA on the same workload. Multiple controllers adjusting the same Deployment's replicas will cause unpredictable behavior.

6. Monitor HPA Itself

Treat HPA as critical infrastructure. Monitor its status, events, and decisions:

# Regularly check HPA state
kubectl get hpa -A -o wide

# Set up alerting on HPA conditions
kubectl get hpa -o json | jq '.items[].status.conditions'

# Log HPA events to your monitoring system
kubectl get events --field-selector involvedObject.kind=HorizontalPodAutoscaler -w

7. Test with Realistic Load Patterns

Before deploying to production, simulate traffic patterns that match your expected workload. Use tools like hey, vegeta, or k6 to generate gradual ramps, sudden spikes, and sustained load. Observe how HPA behaves and tune the stabilization windows and policies accordingly.

8. Set Container Resource Limits Higher Than Requests

HPA uses requests for utilization calculations, but the container actually uses up to limits under load. Ensure limits are sufficiently higher than requests so pods don't get throttled or OOM-killed while HPA is still scaling up:

resources:
  requests:
    cpu: "500m"
    memory: "1Gi"
  limits:
    cpu: "2"
    memory: "4Gi"
# Utilization is calculated as (actual usage / 500m) for CPU percentage.

9. Use Pod Disruption Budgets Alongside HPA

When HPA scales down, it may evict pods. Combined with voluntary disruptions (node drains, upgrades), this could cause temporary unavailability. Define a PodDisruptionBudget to ensure a minimum number of pods are always available:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-app-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: my-app

10. Version Your HPA Manifests in Git

Treat HPA configuration as code. Store YAML manifests alongside your Deployment definitions in the same Git repository. This ensures reproducibility, audit trails, and easy rollbacks.

HPA with StatefulSets and Custom Resources

HPA works with more than just Deployments. You can target StatefulSets, ReplicaSets, and any custom resource that implements the /scale subresource. For StatefulSets, the considerations are slightly different because each pod has a unique identity and possibly persistent storage:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: kafka-consumer-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: StatefulSet
    name: kafka-consumers
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Pods
    pods:
      metric:
        name: kafka_consumer_lag
      target:
        type: AverageValue
        averageValue: "100"

Be cautious scaling StatefulSets down—ensure the pods being terminated have finished processing their assigned partitions or work items. Use graceful shutdown hooks and pod lifecycle management (preStop handlers) to drain work before termination.

Integration with Cluster Autoscaler

HPA and Cluster Autoscaler work beautifully together. HPA increases pod count when demand rises; if the cluster lacks sufficient nodes to schedule those new pods, Cluster Autoscaler provisions additional nodes. When demand drops, HPA reduces pods, and eventually Cluster Autoscaler removes underutilized nodes. This creates a fully elastic infrastructure:

  1. Traffic increases → HPA scales pods → pending pods trigger Cluster Autoscaler → new nodes join → pods scheduled.
  2. Traffic decreases → HPA scales down pods → nodes become underutilized → Cluster Autoscaler removes nodes after cooldown.

For this to work seamlessly, set pod disruption budgets appropriately and ensure your node scaling is fast enough to keep up with HPA's pod creation rate.

Conclusion

The Horizontal Pod Autoscaler is an indispensable tool for running efficient, resilient, and cost-effective workloads on Kubernetes. By understanding its core concepts—metrics sources, target thresholds, stabilization windows, and scaling policies—you can automate capacity management and eliminate manual scaling interventions. Start with simple CPU-based autoscaling, gradually incorporate custom metrics for application-aware scaling, and fine-tune the behavior parameters to match your workload patterns. With careful configuration and adherence to best practices like setting resource requests, configuring stabilization windows, and monitoring HPA status, you'll build systems that gracefully handle traffic surges while minimizing waste during quiet periods. The combination of HPA, Cluster Autoscaler, and a robust metrics pipeline gives you a production-grade, fully autoscaling platform that adapts to demand automatically.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles