The Horizontal Pod Autoscaler (HPA) adds or removes pod replicas based on load: more pods when CPU usage rises, fewer when traffic drops. It’s built into Kubernetes, and takes a few lines of YAML. It needs metrics, though, and realistic resource requests, or it won’t behave as expected.
Background: Kubernetes deployments and services.
1. Make sure metrics are available
The HPA reads CPU and memory usage from metrics-server:
kubectl top nodes
kubectl top pods
If that fails, install metrics-server (k3s includes it by default):
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
On some self-managed clusters, kubelet certificates aren’t signed for node IPs. Then metrics-server needs the --kubelet-insecure-tls argument (acceptable for small private clusters; better to fix the certificates).
2. Set resource requests
CPU utilisation targets are a percentage of the requested CPU. Without requests, the HPA can’t compute utilisation:
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
memory: 512Mi
3. Create the HPA
Quick version:
kubectl autoscale deployment web --cpu-percent=70 --min=2 --max=10
Declarative version (autoscaling/v2):
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # wait 5 min before scaling down
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Pods
value: 4
periodSeconds: 30
kubectl apply -f hpa.yaml
kubectl get hpa
kubectl describe hpa web
If you use an HPA, remove replicas: from the Deployment manifest you apply (or Helm/GitOps keeps resetting it).
4. Test it
Generate load, and watch:
kubectl run load --rm -it --image=busybox -- /bin/sh -c "while true; do wget -q -O- http://web; done"
kubectl get hpa web -w
Replicas increase within a minute or so of sustained high CPU, and scale down after the stabilisation window.
Scaling on other metrics
- Memory:
name: memory, but memory often doesn’t drop when load drops (caches, garbage collectors), so it’s a poor scaling signal for many apps. - Custom metrics (requests per second, queue length) via Prometheus Adapter, or KEDA, which scales on queues (RabbitMQ, Redis, Kafka), cron schedules, and even to zero.
Limits on VPS clusters
The HPA adds pods, not servers. On a fixed set of VPS nodes:
- you can only scale until node CPU and RAM are used up, after which new pods stay Pending;
- the Cluster Autoscaler (adding nodes automatically) needs cloud provider integration, which self-managed VPS clusters usually lack.
Plan node capacity for your peak maxReplicas, or add nodes manually ahead of known peaks. See VPS scaling: vertical vs horizontal and when to upgrade your VPS.
Troubleshooting
| Symptom | Cause |
|---|---|
TARGETS: <unknown>/70% |
metrics-server missing, or no CPU requests set |
| Never scales up | Target too high; app bottleneck isn’t CPU (database, I/O) |
| Flapping up and down | Increase scaleDown.stabilizationWindowSeconds |
| New pods Pending | Nodes are full; add capacity |
Quick reference
| Task | Command |
|---|---|
| Check metrics | kubectl top pods |
| Quick HPA | kubectl autoscale deploy web --cpu-percent=70 --min=2 --max=10 |
| Status | kubectl get hpa, kubectl describe hpa web |
| Required | CPU requests on containers |
| Advanced triggers | KEDA, Prometheus Adapter |
Related: Kubernetes debugging pods and k3s.