Every cluster created with Syself Autopilot runs the [metrics-server](/docs/hetzner/apalla/observability/metrics/metrics-server-and-live-usage), so a Horizontal Pod Autoscaler that scales on CPU or memory works the moment you apply it, with no need to install additional tools. It watches live usage, compares it to a target you set, and moves the replica count until the two line up. ## How it decides The math is a ratio. If your pods average 80% CPU against a 50% target, the HPA figures it needs roughly `80/50` (that is, 1.6) times the replicas it has now, and it adds them. When load drops, it works the same ratio in reverse. ```yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: web spec: scaleTargetRef: # the workload whose replica count the HPA owns apiVersion: apps/v1 kind: Deployment name: web minReplicas: 2 # floor: 2+ so a node drain never leaves you at zero maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 # target is a % of the CPU request, not the node ``` > [!IMPORTANT] > A CPU or memory target is a **percentage of the request**, not of the node. If a container has no CPU request, "60% of CPU" has no denominator and the HPA simply cannot scale it. [Set requests](/docs/hetzner/apalla/workloads/production/resource-requests-and-limits) before you use resource-based HPAs. ## Min, max, and stabilization `minReplicas` and `maxReplicas` set the range the HPA moves within. Between them sits stabilization, which keeps the replica count from flapping: the HPA scales _up_ fast, but by default waits 5 minutes of sustained low load before scaling _down_, so a short drop in usage does not shed capacity you will want back a minute later. Tune `behavior.scaleUp` and `behavior.scaleDown` when the defaults do not fit your traffic shape. ## Your HPA during a node roll `minReplicas: 1` is a common mistake on this platform. Nodes here can get drained and replaced constantly. Every Kubernetes upgrade rolls them out, and self-healing replaces any node that goes unhealthy. When a node drains, its pods are evicted with up to the [180-second default drain window](/docs/hetzner/apalla/workloads/production/graceful-shutdown) to reschedule elsewhere. A single-replica workload has no second copy to carry traffic across that gap, so it can sit at _zero_ running pods mid-drain even though the HPA is behaving exactly as configured. Keep `minReplicas` at 2 or higher for anything that has to stay reachable. ## Beyond CPU and memory CPU and memory are not the only signals an `autoscaling/v2` HPA can read. Point it at a custom metric from your own application, or an external one like queue depth: install an adapter that publishes the value to the Kubernetes metrics API, then reference it under `metrics` the same way you would CPU. One limit stays fixed, though: the HPA only changes pod count, never node count. When it scales past what the current nodes hold, pair it with the [cluster autoscaler](/docs/hetzner/apalla/clusters/configure/autoscale-a-cluster). ## Verify `kubectl get hpa web` shows current usage against your target under `TARGETS`: `35%/60%` means it is reading metrics and has headroom. If you see `/60%`, the HPA cannot read usage at all, almost always a missing CPU request or metrics-server not reaching the pod. Fix that first; a blind HPA will not scale. Then prove the important case: cordon and drain the node running your workload by hand, and watch it. If `minReplicas` is 2 or more, traffic never notices. If it is 1, you will see the gap the next real upgrade would have shown you in production.