Skip to main content

Your HPA Config Is Correct. So Why Won't Pods Scale? The Real Cause of 'Unknown Metrics' in K3s and the HPA/VPA Death Spiral

The HPA YAML Is Correct. Not a Single Pod Gets Added

You configured a HorizontalPodAutoscaler (HPA) on your K3s cluster. kubectl apply succeeded. The CPU utilization threshold and the min/max replica counts are correct no matter how many times you re-read the manifest. Yet when load spikes, not a single Pod gets added. When engineers say "HPA isn't scaling on K3s," the root cause is almost never a bug in HPA itself — 9 times out of 10 it's hiding one layer beneath it, in resource configuration or the metrics pipeline.

section01

If running kubectl get hpa shows the TARGETS column stuck at <unknown>/50%, that's not a sign that HPA is broken — it's a sign that HPA isn't receiving the information it needs. This article walks through the three root causes that show up repeatedly in K3s operations, a diagnostic process that isolates the failing layer in about three minutes, and a design approach that prevents recurrence. Whether you have an observability stack that visualizes autoscaling behavior (Kubo ships with Prometheus + Grafana built in) makes a real difference in how fast you can pin down the cause.

Cause 1: Missing Resource Requests — a Silent Disablement

The most commonly overlooked cause is a missing resources.requests on the container. Kubernetes' official HorizontalPodAutoscaler walkthrough explicitly sets requests.cpu on the target Deployment in its example, because HPA uses that value as the denominator when calculating utilization.

What makes this tricky is that the HPA controller doesn't throw an error when requests are missing. One technical writeup notes that "HPA assumes resource requests are defined, and without them it cannot compute a utilization percentage" (per oneuptime.com's explanation). In other words, this never surfaces as an obvious misconfiguration — the affected metric is silently skipped, and HPA just keeps running in a state where it believes there's nothing to do. You think you're monitoring the workload, but in reality nothing is being measured at all. This silent disablement is the single most common reason HPA fails to fire in K3s production environments.

section02

Cause 2: Metrics Pipeline Latency (kubelet → metrics-server → HPA)

Even when requests are configured correctly, you're not out of the woods yet. The utilization data HPA reads only becomes available after passing through a chain: kubelet → Metrics Server → the HPA controller → the scheduler → Pod startup → the readiness probe. According to the metrics-server FAQ on kubernetes-sigs, the default scrape interval for metrics-server is 60 seconds, and setting it below 15 seconds is not recommended, since that's the resolution at which kubelet itself calculates metrics.

On top of that collection interval, the HPA controller's evaluation cycle, Pod scheduling, container startup, and the time to pass the readiness probe all add up — it's a structural given that anywhere from tens of seconds to a few minutes will elapse between a traffic spike and the moment scaling actually completes. If HPA "feels slow," in most cases that isn't a malfunction; it's simply the physical characteristic of this pipeline, and alert thresholds and load-test scenarios should be designed around that latency from the outset.

section03

The same pipeline is used even on small K3s clusters, so if the configuration requirements documented in the metrics-server GitHub repository — such as enabling the aggregation layer and configuring kubelet certificate validation — aren't met, metrics can't even be collected at the front of the pipeline. Because K3s is a lightweight distribution, some components are sometimes omitted from the default setup, so confirming a healthy metrics-server deployment should be your first diagnostic step.

Cause 3: The HPA/VPA "Death Spiral"

Requests are configured, and metrics are flowing correctly. Yet scaling behavior is still unstable — Pod counts oscillate, or the workload scales out far more aggressively than expected. If that's what you're seeing, suspect a VerticalPodAutoscaler (VPA) running alongside HPA.

ScaleOps' analysis describes the self-reinforcing loop that occurs when HPA and VPA target the same metric. When HPA scales out horizontally, per-Pod load drops, and VPA interprets this as evidence that requests can be lowered. When VPA lowers requests, the denominator in HPA's utilization calculation (the request value) shrinks, so the same absolute usage now looks like a spike in utilization, prompting HPA to scale out even further. This repeating cycle is what's known as the "HPA/VPA death spiral."

section04

VPA's CRD design is built around automatically adjusting requests, and this structural conflict surfaces whenever both controllers run in Auto mode on the same workload without a clear division of responsibility between them.

Diagnostic Steps: Isolating the Bottleneck Layer in Three Minutes

Since the root cause can live in one of three distinct layers, systematically narrowing it down is faster than blindly re-reading YAML. The diagnostic guide from codewithkarani, which walks through checking the Conditions from kubectl describe hpa, is a solid starting flow.

  1. Run kubectl describe hpa <name> -n <namespace> and read the Conditions from top to bottom.AbleToScale: False points to a problem with the scale target reference or RBAC permissions. ScalingActive: False lets you distinguish, via its message, between "no metrics returned" (a metrics-server-side problem) and "missing request for cpu" (unset requests). ScalingLimited: True simply means you've hit the min/max boundary — it isn't a failure.
  2. Check whether metrics-server itself is returning values with kubectl top pods and kubectl top nodes. If no values come back here, the problem is metrics-server's configuration, not HPA.
  3. Check the request configuration directly with kubectl get deployment <name> -o jsonpath='{.spec.template.spec.containers[0].resources}'. If requests is empty, Cause 1 is confirmed.
  4. Check whether a VPA is applied to the same workload. If a VPA object's updateMode is Auto and it's watching the same metric as HPA, suspect the Cause 3 death spiral.

section05

Design Fixes: Setting Requests, Tuning stabilizationWindow, and When to Move to KEDA

Once diagnosis identifies the cause, translate it into a design that prevents recurrence.

  • Set requests based on measured usage. Don't use a low placeholder value — set requests based on actual measured CPU/memory consumption under normal load.
  • Don't let HPA and VPA share the same metric. If HPA is driven by CPU, target VPA at memory only, or set updateMode: "Off" or "Initial" so it stops adjusting automatically and only surfaces recommendations for reference.
  • Tune the scale-down stabilizationWindow. To prevent flapping (rapid scale-up/scale-down cycles), configure a sufficiently long stabilization window on the scale-down side so Pods aren't removed the instant load dips.
  • Consider moving to KEDA if queue length or event counts are your leading indicator. For workloads where CPU/memory alone can't capture the early signal of incoming load (such as message queue processing), the event-driven scaling provided by KEDA is a better fit. KEDA doesn't replace HPA — it's designed to sit above it and extend the set of scaling triggers while coexisting with it.

section06

All of these design changes assume one thing: that metrics are actually visible and correct. Writing the configuration isn't enough to be confident it works — you can only verify the design is functioning once you can actually see request values, utilization, and scaling history in Prometheus or Grafana. Kubo ships with Prometheus + Grafana built in, so you can immediately check metrics-server latency and HPA/VPA scaling behavior on a dashboard.

Conclusion: Autoscaling Isn't "Set and Forget" — It's "Set and Observe"

When HPA won't scale on K3s, the cause is rarely a defect in HPA itself. It usually lives in one of three layers: unset resource requests, a metrics-server configuration gap or collection-interval delay, or a design conflict between HPA and VPA. Starting from the Conditions in kubectl describe hpa to isolate the failing layer lets you skip the blind trial-and-error.

The other key point is that autoscaling isn't something you configure once and walk away from — it only delivers on its promise when you keep observing it continuously. If building an observability stack from scratch feels like too much overhead, it's worth considering a managed Kubernetes offering like Kubo Cloud, which is K3s-based and ships with Prometheus + Grafana out of the box. It costs less than AWS EKS or Azure AKS while staying compatible with standard Kubernetes, letting you run the diagnostic workflow described in this article entirely from a dashboard. If you're not confident in your current autoscaling design, get in touch and we can help you work through it.

Related articles

← Back to all posts