skip to content
‹ All posts

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.

#kubernetes

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.replicas hardcoded in the manifest and re-applied by CI/GitOps every sync: ArgoCD or Flux resets what the HPA just scaled. Remove replicas from 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 -A and check scaleTargetRefs.
  • AbleToScale: False, FailedGetScale: the scaleTargetRef is wrong - name typo, wrong kind (Deployment vs StatefulSet), 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.