HPA not scaling: metrics-server and the usual suspects
HPA not scaling? The ordered checklist: metrics-server health, missing resource requests, misread targets, behavior windows, and what the conditions mean.
An HPA not scaling is a quiet failure. Nothing crashes, nothing goes red - your Deployment just sits at three replicas while latency climbs, and you find out from a dashboard or a page instead of an event. The good news: the HPA writes down exactly why it isn't acting, every reconcile loop. Most people just never read it.
Start here:
kubectl describe hpa <name>
Two sections matter: Conditions and Events. They fall into a small number of patterns, and
each pattern has one fix. Here they are, ordered by how often they're the answer.
1. The metrics pipeline is broken
The HPA can't scale on metrics it can't get. The signature in describe:
Conditions:
AbleToScale True SucceededGetScale
ScalingActive False FailedGetResourceMetric the HPA was unable to compute the
replica count: unable to get metrics for resource cpu
and in the TARGETS column of kubectl get hpa, the giveaway: <unknown>/70%.
Check the pipeline bottom-up:
# Is metrics-server running?
kubectl get pods -n kube-system -l k8s-app=metrics-server
# Is the metrics API actually serving?
kubectl get apiservice v1beta1.metrics.k8s.io
kubectl top pods -n <namespace>
If kubectl top fails, the HPA has nothing to work with. Common causes: metrics-server not
installed at all (it's not part of a default kubeadm cluster), its pod crashlooping on TLS
verification against kubelets (the infamous --kubelet-insecure-tls situation in lab
clusters), or the APIService showing False (FailedDiscoveryCheck). Fix metrics-server first;
the HPA recovers on its own within a minute.
2. Resource requests are unset
This is the one that burns people because everything looks healthy. CPU utilization targets are percentages of the pod's requests. No requests, no denominator:
ScalingActive False FailedGetResourceMetric ... missing request for cpu
The trap: this fails per-pod. One container in the pod without a CPU request - an injected sidecar, say - breaks utilization math for the whole pod. Audit every container:
kubectl get pod <pod> -o jsonpath='{range .spec.containers[*]}{.name}{": "}{.resources.requests}{"\n"}{end}'
Set requests on all containers and the HPA starts computing. What to set them to is its own minefield - see Requests and limits: the misconfigurations that page you.
3. You're reading the target math wrong
The HPA formula is:
desiredReplicas = ceil(currentReplicas × currentMetric / targetMetric)
Two consequences people misread:
- Utilization is averaged across all pods. One pod at 400% CPU and nine idle pods average to ~45% - under a 70% target, no scale-up. The HPA is behaving correctly; your load is imbalanced. That's a Service/load-balancing problem, not an HPA problem.
- There's a tolerance band (10% by default). At target 70% and current 74%, nothing happens. The maths says 1.06× - inside tolerance, no action.
Also check the ceiling: kubectl get hpa showing REPLICAS pinned at MAXPODS means the HPA
did scale and hit the wall. The event says DesiredReplicas ... limited by maxReplicas.
Raise maxReplicas - after confirming the cluster can actually schedule the extra pods,
or you'll trade an HPA problem for pods stuck in
Pending.
4. Behavior windows are slowing you down on purpose
Since autoscaling/v2, behavior controls scaling velocity. The default downscale
stabilization window is 300 seconds - the HPA takes the highest desired replica count from
the last 5 minutes before scaling down. If your complaint is "HPA scales up fine but takes
forever to scale down": that's the design working.
kubectl get hpa <name> -o jsonpath='{.spec.behavior}' | jq
If your complaint is the opposite - flapping, scaling up and down every minute - you want those windows. Tune deliberately:
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
5. Something else owns the replica count
An HPA fighting another controller loses, or worse, ties forever:
spec.replicashardcoded in the manifest and re-applied by CI/GitOps every sync: ArgoCD or Flux resets what the HPA just scaled. Removereplicasfrom the manifest when an HPA manages the workload (and configure your GitOps tool to ignore the field).- A second HPA targeting the same Deployment.
kubectl get hpa -Aand checkscaleTargetRefs. AbleToScale: False, FailedGetScale: thescaleTargetRefis wrong - name typo, wrong kind (DeploymentvsStatefulSet), or wrong apiVersion. The HPA can't even find the thing it's supposed to scale.
When it's not the HPA at all
Scale-up "not working" sometimes means: the HPA raised replicas, and the new pods never became
ready. kubectl get pods shows them Pending (no node capacity - you need cluster-autoscaler
too) or 0/1 Ready (failing readiness probes under load). The HPA did its job; the rollout
machinery didn't. Different debugging tree - start at
Rolling update stuck for the readiness half.
And if you're scaling on queue depth, request rate, or anything that isn't CPU/memory, the resource-metrics HPA is the wrong tool wearing the right name - you want custom metrics via an adapter, or KEDA, which wraps this whole problem in event-driven scalers and scales to zero. Practicing this failure class deliberately - break metrics-server yourself, watch the conditions change - teaches more than any post; there's a curriculum for that in Learn Kubernetes by breaking it.
FAQ
Why does my HPA show <unknown> in the TARGETS column?
The metrics pipeline is broken for that metric: metrics-server down or absent, the
v1beta1.metrics.k8s.io APIService unavailable, or pods missing resource requests. Run
kubectl top pods - if that fails, fix metrics before touching the HPA.
How often does the HPA check metrics?
The controller reconciles roughly every 15 seconds (--horizontal-pod-autoscaler-sync-period
on the controller-manager). Metrics-server itself scrapes at its own interval, so end-to-end
reaction time is typically 15–60 seconds - before any stabilization windows you've configured.
Can the HPA scale to zero?
Not on resource metrics - minReplicas must be ≥ 1 unless you enable alpha gates for
object/external metrics. Scale-to-zero on real workloads is KEDA's job.
Does HPA work with memory targets?
Yes (resource: memory in autoscaling/v2), but think twice: most runtimes don't return memory
under reduced load (GC heaps hold their high-water mark), so memory-based HPAs scale up and
never back down. CPU or a work-derived custom metric is usually the better signal.