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:
- Fetch metrics for all pods belonging to the target resource
- Calculate the desired replica count using the formula:
desiredReplicas = ceil( currentReplicas * (currentMetricValue / desiredMetricValue) ) - Update the target resource with the new replica count (bounded by min and max limits)
Key Terminology
- Target resource: The Deployment, ReplicaSet, or StatefulSet that HPA monitors and scales.
- Metrics: The measurements HPA uses to make scaling decisions (CPU utilization, memory usage, custom metrics, or external metrics).
- Target metric value: The threshold you want to maintain (e.g., 50% CPU utilization).
- Current metric value: The actual observed metric across all pods.
- Min replicas / Max replicas: The floor and ceiling for scaling, preventing under-scaling or runaway scaling.
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:
- Over-provisioning: Run enough pods to handle peak load at all times, wasting compute resources and money during off-peak hours.
- Under-provisioning: Run fewer pods and risk downtime, slow responses, and unhappy users when traffic spikes.
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:
- Cost efficiency: You pay for only the resources you need at any given moment.
- Reliability: Your application stays responsive under load without manual intervention.
- Resilience: Combined with cluster autoscaling, even the underlying node pool can grow and shrink automatically.
- Operational simplicity: No more pager alerts at 2 AM because someone forgot to scale up before a marketing campaign.
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:
- Scale-up stabilization window: The HPA looks back over this window and picks the maximum recommended replica count, preventing over-eager scaling from a single metric spike.
- Scale-down stabilization window: The HPA looks back and picks the minimum recommended replica count, preventing premature scale-down from a brief lull in traffic.
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:
Max(typically for scale-up): Applies the policy that allows the largest change, ensuring rapid response to demand.Min(typically for scale-down): Applies the policy that allows the smallest change, ensuring cautious scale-down.Disabled: Prevents scaling entirely in that direction.
Policy Types Explained
- Percent policy: Scales by a percentage of current replicas. A value of 100 means "double the current count" (for scale-up) or "halve the current count" (for scale-down) within the specified
periodSeconds. - Pods policy: Scales by a fixed absolute number of pods within the period.
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
- Missing resource requests: If your pods lack
resources.requests, utilization percentages cannot be calculated. Always set resource requests on HPA-managed pods. - Max replicas reached: Check if
maxReplicasis already hit. The HPA will not exceed this bound. - Stabilization window blocking: A large
stabilizationWindowSecondsfor scale-up may delay scaling decisions. - Single metric spike not sustained: HPA has an internal tolerance (default 10%) to prevent scaling from minor fluctuations.
HPA Scales Too Aggressively (Flapping)
- Stabilization window too short: Increase
stabilizationWindowSecondsfor scale-down. - Scale-down policy too aggressive: Use a smaller percentage or fewer pods per period.
- Metric is too volatile: Consider using a smoothed metric or a different metric type that better represents sustained load.
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:
- Scale-up: 0–60 seconds for fast reaction to legitimate traffic increases.
- Scale-down: 300–600 seconds (5–10 minutes) to prevent thrashing from intermittent dips.
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:
- Traffic increases → HPA scales pods → pending pods trigger Cluster Autoscaler → new nodes join → pods scheduled.
- 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.