Horizontal Pod Autoscaler: The Algorithm and Its Traps
One formula decides your replica count. Knowing its inputs is most of the work.
The Horizontal Pod Autoscaler (HPA) is a built-in Kubernetes API resource and controller that changes the replica count of a workload, such as a Deployment or StatefulSet, to match observed demand. Horizontal means more or fewer pods; vertical scaling, by contrast, would change the CPU and memory given to each pod. HPA does not apply to objects that cannot be scaled, such as a DaemonSet.
The control loop
The HPA controller runs inside the controller manager and checks each HPA once per sync period, which defaults to 15 seconds (--horizontal-pod-autoscaler-sync-period). For every metric it computes:
desiredReplicas = ceil(currentReplicas × currentMetricValue / desiredMetricValue)
So 4 replicas averaging 100% of their CPU request, against a 50% target, gives 8. If the ratio is close enough to 1.0 (within a tolerance of 0.1 by default) the controller does nothing, which prevents constant small corrections. When several metrics are configured, it calculates a replica count for each and uses the largest.
Where the metrics come from
HPA reads three aggregated APIs:
metrics.k8s.iofor resource metrics (CPU and memory), usually served by Metrics Server as a cluster add-on.custom.metrics.k8s.iofor per-object custom metrics, served by an adapter for your monitoring system.external.metrics.k8s.iofor metrics from outside the cluster, such as a queue length.
Scaling on memory and custom metrics requires the autoscaling/v2 API, stable since Kubernetes 1.23. Without an installed adapter, the custom and external APIs simply do not exist.
Why resource requests are mandatory
Utilization targets are percentages of the pod’s resource request. The Kubernetes documentation is explicit: if any container in the pod lacks the relevant request, CPU utilization for the pod is undefined and the autoscaler takes no action for that metric. A Deployment without requests will sit at its current size no matter how busy it is. The fix, and its side effects on scheduling, are covered in pod resource requests and limits.
Keeping it stable
Scale-down is damped by a stabilization window: the controller uses the highest recommendation seen within the window, which defaults to 5 minutes (--horizontal-pod-autoscaler-downscale-stabilization, or behavior.scaleDown.stabilizationWindowSeconds: 300 per HPA). The behavior field also lets you cap how many pods or what percentage can be added or removed per period.
A common self-inflicted problem is keeping spec.replicas in the Deployment manifest. Every kubectl apply resets the replica count to that value, fighting the HPA and causing thrashing. The Kubernetes docs recommend removing spec.replicas once an HPA manages the workload.
HPA does not add nodes
HPA only creates pods. If the cluster lacks room, the new pods stay pending until a node autoscaler, such as Cluster Autoscaler or Karpenter, provisions nodes for unschedulable pods. Both node autoscalers size nodes from pod resource requests, not actual usage, which is another reason requests need to be realistic.
Scaling to zero
minReplicas: 0 is allowed only when at least one object or external metric is configured; the API server rejects the HPA otherwise. The behaviour is controlled by the HPAScaleToZero feature gate, which was alpha and off by default from Kubernetes 1.16 to 1.36 and is beta and on by default from 1.37. It must be enabled on both the API server and the controller manager, so check your cluster version before relying on it.
What ZopNight checks
ZopNight’s Kubernetes workload rules include reliability checks for missing requests and limits, single-replica workloads, and HPAs pinned at their maximum, plus rightsizing that uses HPA ScalingLimited signals. When a namespace is stopped on a schedule, its HPAs and PodDisruptionBudgets are removed and then restored on start; see namespace-level scheduling. The HPA integration page lists what is discovered.
Questions we get a lot.
If yours isn't here, email us and we'll answer directly.
Why is my HPA showing unknown CPU and never scaling?
Usually because one or more containers in the pod have no CPU request. Utilization is calculated against the request, so without it the metric is undefined and the HPA takes no action. Also confirm that the metrics.k8s.io API is served, typically by Metrics Server.
How often does the HPA re-evaluate?
Once per sync period, 15 seconds by default. Scale-down decisions are further smoothed by a stabilization window that defaults to 5 minutes.