A Horizontal Pod Autoscaler configured on a deployment whose pods have no CPU requests set never scales correctly, and that’s not a minor detail: it’s the silent precondition that breaks the whole mechanism before a single line of HPA configuration ever gets read.
The calculation depends on a percentage, not an absolute value
The HPA targets a resource utilization percentage, calculated relative to the request set on the container, not relative to machine capacity or some absolute value.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: payment-api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: payment-api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
That 70% target means “70% of the CPU requested” via requests and limits, not 70% of a physical core. Without a request set, the HPA simply has no baseline to calculate against, and the controller cannot produce a coherent scaling decision - this is the most common reason an HPA that looks correctly configured never actually scales. The other silent prerequisite, one level further upstream: without metrics-server installed on the cluster, the HPA has no data to read at all, regardless of how correctly requests are set.
How the controller decides on a replica count
The HPA controller periodically polls (every 15 seconds by default) current utilization through metrics aggregated over a time window, and calculates the desired replica count with a simple formula: desired replicas = current replicas × (current utilization / target utilization), rounded up. Load at 140% of target on 4 replicas gives 4 × (140/70) = 8 desired replicas.
The flapping trap
Without a safeguard, a system that scales up the moment load exceeds the target and scales down the moment it drops back below can oscillate in a loop: scale up, load mechanically drops with more replicas, scale down, load rises again, scale up again. Every cycle has a real cost (new pods starting, a window where they aren’t ready yet) without ever stabilizing the actual load.
The controller has a default stabilization window built in: it only accepts a scale-down after a multi-minute window with no scale-up need, which absorbs one-off spikes without triggering oscillation. This behavior is finely configurable:
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
What the HPA does not solve
The HPA scales the number of pods, never their individual resources (that’s the Vertical Pod Autoscaler’s job, a separate mechanism). It also solves nothing if the underlying node lacks capacity to host new pods: that’s where the cluster autoscaler (at the node level, not the pod level) needs to step in as a complement. A deployment with a single replica gets no meaningful benefit from autoscaling either: minReplicas should be at least 2 for the mechanism to make sense, and for high availability to hold during a scaling operation.
Where this fits
Setting honest requests isn’t just about scheduling or eviction, as covered in the requests and limits article: it’s also the silent prerequisite without which horizontal autoscaling doesn’t work at all. Getting this chain right is part of the foundation laid during a Kubernetes migration.
What to remember
The HPA calculates its decisions from a percentage of the request set on the container, never an absolute value: without a request defined, no coherent scaling is possible. The scale-down stabilization window prevents flapping. The HPA scales pod count, neither individual pod resources nor underlying node capacity.