How to Set Up Horizontal Pod Autoscaling in Kubernetes
Configure Kubernetes HPA with verified metrics, resource requests, scaling behavior, load tests, and capacity safeguards for reliable autoscaling.
This sets up Horizontal Pod Autoscaling (HPA) end to end — including the metrics infrastructure it depends on, which is the step most commonly missing when an HPA configuration silently does nothing.
Step 1: confirm metrics-server is installed
HPA needs resource metrics to make scaling decisions, and those come from metrics-server, which isn’t installed by default on every cluster:
kubectl get deployment metrics-server -n kube-system
If it’s missing, install it:
kubectl apply -f \
https://github.com/kubernetes-sigs/metrics-server/releases/download/VERSION/components.yaml
Replace VERSION with a reviewed Metrics Server release compatible with the cluster and verify the published manifest before applying it. Managed Kubernetes services may provide metrics through a supported add-on; follow the provider’s installation path instead of layering an unmanaged copy over it. Do not use insecure kubelet TLS flags as a permanent fix for certificate problems.
Step 2: verify metrics are actually flowing
kubectl top nodes
kubectl top pods
Don’t proceed until this returns real numbers. If it errors or returns nothing, HPA has no data to scale on and will silently fail to do anything useful, regardless of how correctly the HPA resource itself is configured.
Step 3: make sure your deployment declares resource requests
HPA calculates CPU utilization as a percentage of the pod’s requested CPU — without a request set, there’s no baseline to calculate a percentage against:
# deployment.yaml
spec:
template:
spec:
containers:
- name: myapp
image: registry.example.com/myapp@sha256:IMAGE_DIGEST
resources:
requests:
cpu: "200m"
limits:
cpu: "500m"
Step 4: create the HPA resource
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
This scales myapp between 2 and 10 replicas, aiming to keep average CPU utilization around 70% of the requested 200m per pod.
If a relevant container has no CPU request, the controller cannot calculate utilization for that pod as expected, which can suppress scaling decisions. Requests must reflect measured workload behavior: setting them artificially low makes normal work look overloaded, while setting them too high wastes capacity and can prevent scheduling. Include sidecars in the analysis because they can consume CPU without tracking application demand.
kubectl apply -f hpa.yaml
Step 5: verify the HPA sees current metrics
kubectl get hpa myapp-hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
myapp-hpa Deployment/myapp 23%/70% 2 10 2
A TARGETS column showing <unknown>/70% instead of an actual percentage means metrics still aren’t reaching the HPA controller — revisit steps 1–3 rather than waiting, since this state won’t resolve on its own.
Inspect conditions and events for the actual reason:
kubectl describe hpa myapp-hpa
kubectl get --raw /apis/metrics.k8s.io/v1beta1/pods
The HPA control loop is periodic, and fresh pods may not contribute normal CPU samples immediately. Readiness and startup behavior influence CPU initialization handling; configure truthful probes instead of weakening the controller’s safety logic.
Step 6: generate load and watch it scale
kubectl run -it --rm load-generator \
--image=registry.example.com/test-tools@sha256:TEST_IMAGE_DIGEST -- /bin/sh -c \
"while true; do wget -q -O- http://myapp; done"
kubectl get hpa myapp-hpa --watch
Watching the REPLICAS column climb as TARGETS exceeds 70% confirms the whole pipeline — metrics, resource requests, and HPA configuration — is genuinely working end to end, not just correctly configured on paper.
Run load only in an authorized test environment and use a bounded request count or timeout. Verify not only that replicas increase, but that new pods schedule, become Ready, receive traffic, and reduce customer-visible latency. HPA cannot help when the cluster has no spare nodes, quotas block pods, the image cannot pull, or a dependency such as the database is already saturated.
Step 7: understand scale-down behavior
HPA deliberately scales down more conservatively than it scales up, to avoid flapping — by default waiting for a stabilization window before reducing replica count, configurable if the defaults don’t fit your workload:
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
The default downscale stabilization window is five minutes. Explicit behavior is useful when a workload has expensive startup, bursty traffic, or downstream limits, but tune it from measurements. Faster scale-up can overload a database or queue consumer dependency; slower scale-down costs more but reduces oscillation. Maintain a PodDisruptionBudget for voluntary disruptions and enough baseline replicas for availability, while remembering a PDB does not control HPA scaling.
Step 8: consider scaling on custom metrics, if CPU isn’t the right signal
For workloads where CPU isn’t a good proxy for actual load (a queue-processing service, for instance, where queue depth matters more), HPA supports custom and external metrics via the metrics adapter pattern — a more advanced setup than CPU-based scaling, but built on the exact same HorizontalPodAutoscaler resource type shown here.
With multiple metrics, Kubernetes calculates a desired replica count for each and uses the largest recommendation, subject to error-handling behavior. Choose signals that represent demand, not symptoms that additional replicas cannot solve. Queue depth per consumer, concurrent requests, or scheduled work can be better than CPU; memory often cannot fall until a process exits and therefore needs especially careful testing.
Define safe operating bounds
minReplicas must cover normal availability and disruptions. maxReplicas is a safety boundary, not a capacity plan: validate that node capacity, quotas, network addresses, load balancers, and dependencies can support it. Alert when the HPA remains at maximum, reports invalid metrics, or repeatedly oscillates. Those states mean the autoscaler is exposing an architectural limit, not solving it.
Why metrics-server is the step people actually get stuck on
An HPA resource can be configured perfectly and still do absolutely nothing if metrics-server isn’t installed or isn’t reporting correctly — this is, by a wide margin, the most common reason “I set up HPA and it’s not scaling” turns out to have a simple explanation. Checking kubectl top pods returns real numbers before troubleshooting the HPA resource itself saves considerable confusion.
Related:
- KEDA Event-Driven Autoscaling: ScaledObjects, ScaledJobs, HPA, and Safe Scale-to-Zero
- How to Configure Pod Disruption Budgets in Kubernetes
Sources: