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.

  • Kubernetes
  • Scaling
  • DevOps