Horizontal Pod Autoscaling in Kubernetes
A practical guide to Kubernetes Horizontal Pod Autoscaling: HPA resources, metrics server setup, CPU and memory-based scaling, custom metrics, scaling behavior tuning, and production best practices.
What you'll learn
- ✓How the HPA controller loop works
- ✓Setting up Metrics Server for CPU and memory metrics
- ✓Configuring CPU-based and memory-based autoscaling
- ✓Using custom and external metrics for scaling decisions
- ✓Tuning scaling behavior to prevent flapping
Prerequisites
- •Kubernetes fundamentals (Pods, Deployments, Services)
- •Understanding of resource requests and limits
- •Basic kubectl usage
Horizontal Pod Autoscaling (HPA) automatically adjusts the number of Pod replicas based on observed metrics. When traffic increases and CPU usage rises, HPA adds more Pods. When traffic drops, it removes them. This is the primary mechanism for elastic scaling in Kubernetes, and getting it right means the difference between wasting money on idle Pods and dropping requests during traffic spikes.
How HPA works
The HPA controller runs a control loop every 15 seconds (configurable). In each iteration, it:
- Fetches the current metric values for all Pods in the target Deployment
- Calculates the desired replica count using the formula:
desired = ceil(current * (currentMetricValue / targetMetricValue)) - Scales the Deployment if the desired count differs from the current count
Current: 4 replicas, each at 75% CPU
Target: 50% CPU
Desired = ceil(4 * (75 / 50)) = ceil(4 * 1.5) = ceil(6) = 6 replicas
The HPA scales the Deployment to 6 replicas. On the next check, if CPU drops to 50%, no scaling happens. If it drops to 25%, the HPA calculates ceil(6 * (25 / 50)) = 3 and scales down.
Prerequisites: Metrics Server
HPA requires a metrics source. For CPU and memory metrics, you need Metrics Server installed in the cluster.
# Check if metrics-server is running
kubectl get deployment metrics-server -n kube-system
# Install metrics-server (if not present)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# Verify it works
kubectl top nodes
kubectl top pods
On local clusters (minikube, kind), you may need to add --kubelet-insecure-tls to the metrics-server deployment args.
# For minikube
minikube addons enable metrics-server
Basic HPA: CPU-based scaling
First, your Deployment must have CPU resource requests defined. Without requests, HPA cannot calculate utilization percentages.
# deployment.yaml
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
image: my-web-app:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: "200m" # 0.2 CPU cores
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: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50 # Target 50% CPU utilization
kubectl apply -f deployment.yaml
kubectl apply -f hpa.yaml
# Or create with kubectl directly
kubectl autoscale deployment web-app \
--min=2 --max=10 --cpu-percent=50
# Monitor HPA status
kubectl get hpa web-app-hpa -w
The output shows current utilization, target, and replica count:
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web-app-hpa Deployment/web-app 23%/50% 2 10 2 5m
Memory-based scaling
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70
When multiple metrics are specified, HPA calculates the desired replica count for each metric and uses the highest value. If CPU says 4 replicas and memory says 6, HPA scales to 6.
Memory-based scaling can be tricky because many applications (especially JVM-based) allocate memory at startup and never release it. This means the HPA might scale up but never scale down. Use memory scaling cautiously and prefer CPU for most workloads.
Absolute value targets
Instead of utilization percentages, you can target absolute metric values.
metrics:
- type: Resource
resource:
name: cpu
target:
type: AverageValue
averageValue: 100m # Target 100 millicores per pod
- type: Resource
resource:
name: memory
target:
type: AverageValue
averageValue: 200Mi # Target 200Mi per pod
Absolute targets are useful when you want consistent per-pod resource usage regardless of how big the requests are.
Custom metrics
For more sophisticated scaling, you can use custom metrics from your application. This requires a custom metrics adapter (like Prometheus Adapter).
Setting up Prometheus Adapter
helm install prometheus-adapter prometheus-community/prometheus-adapter \
--namespace monitoring \
--set prometheus.url=http://prometheus-server.monitoring.svc \
--set prometheus.port=9090
# prometheus-adapter-config.yaml (simplified)
rules:
- seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
resources:
overrides:
namespace: { resource: "namespace" }
pod: { resource: "pod" }
name:
matches: "^(.*)_total$"
as: "${1}_per_second"
metricsQuery: 'rate(<<.Series>>{<<.LabelMatchers>>}[2m])'
HPA with custom metrics
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:
# Scale based on requests per second per pod
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "100" # Target 100 RPS per pod
External metrics
Scale based on metrics that are not tied to Kubernetes resources, like a message queue depth.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: queue-worker-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: queue-worker
minReplicas: 1
maxReplicas: 50
metrics:
- type: External
external:
metric:
name: sqs_queue_length
selector:
matchLabels:
queue: "processing-queue"
target:
type: AverageValue
averageValue: "10" # 10 messages per pod
When the queue has 500 messages, HPA calculates 500 / 10 = 50 and scales to 50 pods (capped at maxReplicas).
Scaling behavior
The behavior field (autoscaling/v2) gives fine-grained control over how fast scaling happens.
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: 50
behavior:
scaleUp:
stabilizationWindowSeconds: 60 # Wait 60s before scaling up
policies:
- type: Percent
value: 100 # Double the replicas
periodSeconds: 60
- type: Pods
value: 4 # Or add 4 pods
periodSeconds: 60
selectPolicy: Max # Use whichever adds more pods
scaleDown:
stabilizationWindowSeconds: 300 # Wait 5 min before scaling down
policies:
- type: Percent
value: 10 # Remove 10% of replicas
periodSeconds: 60
selectPolicy: Min # Conservative: remove fewer pods
Key settings:
- stabilizationWindowSeconds — How long the HPA waits before acting on a scale decision. Prevents flapping. Default: 0 for scale up, 300 for scale down.
- policies — Rate limits for scaling. Multiple policies use
selectPolicyto choose. - selectPolicy —
Max(most aggressive),Min(most conservative), orDisabled(no scaling in this direction).
Preventing scale-down flapping
The default 5-minute stabilization window for scale-down is often too short for applications with bursty traffic. Increase it for workloads with irregular patterns.
behavior:
scaleDown:
stabilizationWindowSeconds: 600 # 10 minutes
policies:
- type: Pods
value: 1
periodSeconds: 300 # Remove at most 1 pod every 5 minutes
Aggressive scale-up for latency-sensitive apps
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # Scale up immediately
policies:
- type: Percent
value: 200 # Triple the replicas if needed
periodSeconds: 30
Testing HPA with load generation
# Generate CPU load to trigger scaling
kubectl run load-generator --image=busybox:1.36 --restart=Never -- \
/bin/sh -c "while true; do wget -q -O- http://web-app:8080; done"
# Watch the HPA respond
kubectl get hpa web-app-hpa -w
# Clean up
kubectl delete pod load-generator
Troubleshooting
# Check HPA status and events
kubectl describe hpa web-app-hpa
# Common issues:
# "unable to fetch metrics" — Metrics Server not running
# "missing request for cpu" — No CPU requests on containers
# "<unknown>/50%" — Metrics not available yet (wait ~60s)
# Verify metrics are available
kubectl top pods -l app=web-app
# Check metrics API directly
kubectl get --raw "/apis/metrics.k8s.io/v1beta1/namespaces/default/pods" | jq .
Best practices
- Always set resource requests. HPA cannot calculate utilization without them.
- Start with CPU-based scaling. It is the most predictable and well-understood metric.
- Set sensible min/max bounds. minReplicas should handle your baseline traffic. maxReplicas should be capped by your budget and cluster capacity.
- Use aggressive scale-up, conservative scale-down. It is better to have a few extra pods for a few minutes than to drop traffic during a spike.
- Combine HPA with PodDisruptionBudgets. Ensure that scale-down does not violate your availability requirements.
- Do not use HPA with Deployments that also have manual replica counts managed by CI/CD. They will fight each other.
HPA is the standard mechanism for elastic scaling in Kubernetes. Start simple with CPU-based scaling, add custom metrics as your observability matures, and tune the scaling behavior to match your application’s traffic patterns.
Related articles
- Kubernetes Kubernetes DaemonSets Explained with Practical Examples
Learn how DaemonSets run exactly one pod per node for logging, monitoring, and networking agents. Includes tolerations and update strategies.
- Kubernetes Kubernetes Persistent Volumes and Storage Classes
Understand PersistentVolumes, PersistentVolumeClaims, and StorageClasses to give your Kubernetes workloads reliable, portable storage.
- Kubernetes Kubernetes Network Policies: Pod-to-Pod Security Guide
Lock down pod communication with Kubernetes NetworkPolicies. Learn ingress and egress rules, label selectors, and namespace isolation.
- Kubernetes Kubernetes Resource Limits, Requests, and QoS Classes
Set CPU and memory requests and limits correctly. Understand QoS classes, OOMKilled errors, and CPU throttling in Kubernetes.