On this page
  1. Before you start
  2. Step 1 — Install the metrics server and prove it works
  3. Step 2 — Set requests on the Deployment
  4. Step 3 — Create the HorizontalPodAutoscaler
  5. Step 4 — Load test it and watch it scale
  6. Step 5 — Tune, then confirm it scales back down
  7. Where AI coding assistants get this wrong
  8. Checklist
  9. FAQ
    1. Why does my HPA show TARGETS as unknown?
    2. How many replicas should minReplicas be?
    3. Can an HPA scale on memory instead of CPU?
  10. Related topics
  11. Sources
tutorial

How to Set Up Horizontal Pod Autoscaling (HPA) in Kubernetes

Install the metrics server, set resource requests, create an HPA, and load test it until it actually scales — plus the tuning that stops it flapping and the checks that catch a silent failure.

Quick answer

  • The order is fixed: metrics server, then resource requests, then the HPA — skip either prerequisite and the autoscaler reports unknown metrics forever.
  • kubectl top pods is the one command that proves the metrics API is working before you debug anything else.
  • An HPA averages utilization across pods, so a single hot pod does not trigger a scale-up on its own.
  • Load test it: an HPA you have never seen scale up is an HPA you have not verified.
  • Set a scale-down stabilization window so a brief traffic dip does not remove the capacity the next spike needs.

Before you start

You need a running cluster, a Deployment you can safely run more than one replica of, and a way to generate load. If the app writes to a local file, holds a lock, or runs a background scheduler, fix that first — horizontal scaling means several copies of your process running at once, and anything stateful inside the container will conflict with itself. Confirm the container declares CPU requests, because the HPA measures utilization as a percentage of the request. Finally, decide the ceiling: an HPA can ask for more replicas than your nodes can schedule, and those extra pods will simply sit Pending.

Step 1 — Install the metrics server and prove it works

The HPA reads CPU and memory from the metrics API, so install the metrics server first. Managed platforms generally have it; k3s and many hand-built clusters do not. Apply the upstream manifest and wait for the deployment to become available, then run kubectl top, which is the check that actually matters.

# Install the metrics server
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl -n kube-system rollout status deployment/metrics-server

# This MUST return numbers before an HPA will ever work
kubectl top pods

Step 2 — Set requests on the Deployment

Utilization is measured against requests, so the request is what the HPA divides by. Set it from observed usage rather than a guess: run the app under normal load, read kubectl top, and pick a request near steady state. A request that is far too high makes the app look idle while it is saturated; one that is far too low makes the HPA add replicas it does not need.

spec:
  template:
    spec:
      containers:
        - name: api
          image: ghcr.io/acme/api:1.4.2
          resources:
            requests:
              cpu: 100m      # the denominator for HPA utilization
              memory: 128Mi
            limits:
              memory: 256Mi

Step 3 — Create the HorizontalPodAutoscaler

You can create an HPA imperatively for a quick test, but write the manifest for anything you keep, because it is the only place to set scaling behavior. The behavior block controls how fast the autoscaler scales in and out; the scale-down stabilization window is what prevents flapping during a brief lull.

kubectl autoscale deployment api --cpu-percent=70 --min=2 --max=10   # quick test
kubectl get hpa api -w                                                  # watch TARGETS and REPLICAS

Step 4 — Load test it and watch it scale

An HPA that has never been observed scaling up is unverified. Generate load against the Service from inside the cluster, then watch the HPA’s target column climb above the threshold and the replica count follow. If the target stays at unknown, the metrics API is not working; if the target climbs but replicas do not, check for a missing request or a maxReplicas that is already reached; if replicas appear but sit Pending, the nodes are out of capacity.

# Generate load from a throwaway pod
kubectl run load --rm -it --image=busybox --restart=Never -- \
  sh -c 'while true; do wget -q -O- http://api.default.svc.cluster.local/ >/dev/null; done'

# In another terminal
kubectl get hpa api -w        # TARGETS should climb past 70%, then REPLICAS should rise
kubectl get pods -l app=api   # confirm the new pods reach Running and Ready

Step 5 — Tune, then confirm it scales back down

Stop the load generator and watch the replica count fall. Scaling in is deliberately slower than scaling out: the default stabilization window has the controller look back at recent recommendations and keep the highest, which is what stops a one-minute dip from tearing down capacity. If you see the app oscillating between sizes, lengthen the window rather than lowering the threshold. If it never scales in, check that minReplicas is not holding the floor and that something else — an interrupted rollout or a failing probe — is not pinning the pods.

Where this bites vibecoders

An assistant asked to add autoscaling produces the HPA manifest and stops there, because the manifest is the visible artifact. The two prerequisites live outside it: a metrics server the cluster may not have, and CPU requests the Deployment may not declare. Either one missing produces an HPA that applies cleanly, shows up in kubectl get hpa, and never does anything — with status text that reads like a configuration nit rather than the reason scaling is dead. Installing the metrics server and running kubectl top pods before touching the HPA is the whole difference between autoscaling that works and autoscaling that looks installed.

Where AI coding assistants get this wrong

  • Writing the HPA without checking that the metrics server is installed.
  • Leaving the CPU request unset, so utilization can never be computed.
  • Copying a target utilization of 70 or 80% without measuring what one replica can sustain.
  • Setting maxReplicas far beyond what the nodes can schedule.
  • Autoscaling a Deployment that writes to a local volume or runs an in-process scheduler.
  • Never load testing, so an autoscaler that cannot scale passes for one that does.

Checklist

  • Install the metrics server and confirm kubectl top pods returns values.
  • Set CPU requests from measured steady-state usage.
  • Verify the app is safe to run at two or more replicas.
  • Create the HPA with a minReplicas above one for availability.
  • Load test and watch REPLICAS actually increase.
  • Confirm it scales back down after the load stops.
  • Set maxReplicas within what the cluster can schedule.

FAQ

Why does my HPA show TARGETS as unknown?

The HPA cannot read metrics, which almost always means the metrics server is missing or unhealthy. Run kubectl top pods to confirm, and check the metrics-server pods in kube-system. A container without a CPU request also produces unknown, because there is no denominator for the percentage.

How many replicas should minReplicas be?

At least two for anything that has to stay available, and enough to absorb normal traffic without scaling. Autoscaling is for the spikes above that baseline, not for the baseline itself; leaving it at one means every scale-up starts from a position where a restart is an outage.

Can an HPA scale on memory instead of CPU?

Yes, with autoscaling/v2 you can target memory utilization or several metrics at once. Be careful with memory: most runtimes do not release memory back, so a workload tends to scale up and never down. CPU remains the more predictable signal for most services.

Sources

Share: