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.