<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Hpa · XB Field Notes</title><link>https://www.xbcatm.com/tags/hpa/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/hpa/index.xml" rel="self" type="application/rss+xml"/><item><title>The HPA grows and shrinks a Deployment by load</title><link>https://www.xbcatm.com/posts/kubernetes-hpa-autoscale-workloads/</link><pubDate>Wed, 11 Aug 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/kubernetes-hpa-autoscale-workloads/</guid><description>A HorizontalPodAutoscaler watches a metric and scales replicas to keep utilization in a target band.</description><content:encoded>&lt;p&gt;A HorizontalPodAutoscaler is a feedback controller, not a capacity planner. Once every sync period — 15 seconds by default — it asks the resource metrics API for per-pod CPU, divides the observed value by the target to get a ratio, and writes the result into the target workload&amp;rsquo;s scale subresource. The formula is one line: &lt;code&gt;desiredReplicas = ceil(currentReplicas * currentMetricValue / desiredMetricValue)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Everything interesting is in the qualifiers. The controller deliberately ignores small ratios, deliberately delays shrinking, and refuses to act at all when it cannot read a metric or cannot compute a denominator. Those behaviours make it stable, and they are also why a correctly configured-looking HPA can sit at a fixed replica count while the service burns.&lt;/p&gt;
&lt;h2 id="the-ratio-and-the-dead-band"&gt;The ratio and the dead band&lt;/h2&gt;
&lt;p&gt;For a utilization target the denominator is not a percentage but a number of millicores: a container with a &lt;code&gt;200m&lt;/code&gt; CPU request and a 60% target produces a desired metric of &lt;code&gt;120m&lt;/code&gt;. The ratio is then compared against 1.0 with a configurable tolerance, &lt;code&gt;0.1&lt;/code&gt; by default, and the controller skips scaling entirely when the ratio is inside that band. The band is not a rounding detail; it is the anti-thrash mechanism.&lt;/p&gt;
&lt;p&gt;The band means the observable behaviour is coarse. With a &lt;code&gt;120m&lt;/code&gt; desired metric, average usage of &lt;code&gt;110m&lt;/code&gt;, &lt;code&gt;120m&lt;/code&gt; and &lt;code&gt;132m&lt;/code&gt; all leave the replica count exactly where it was; &lt;code&gt;133m&lt;/code&gt; is the first value that moves it. The step itself is a ceiling, so &lt;code&gt;160m&lt;/code&gt; against &lt;code&gt;120m&lt;/code&gt; rounds three replicas up to four rather than to 3.99 — a fractional pod cannot exist, so the controller always errs on the side of capacity.&lt;/p&gt;
&lt;h2 id="missing-metrics-and-the-veto"&gt;Missing metrics and the veto&lt;/h2&gt;
&lt;p&gt;Pods whose metrics have not arrived are set aside rather than counted as zero, and the controller then re-adds them conservatively: a missing pod is assumed to consume the full target when the decision is to scale down, and nothing when the decision is to scale up. That asymmetry shrinks the magnitude of any move in either direction. A pod with no CPU request has no denominator, so its utilization is undefined and the HPA takes no action for that metric — the commonest cause of an HPA that appears to do nothing.&lt;/p&gt;
&lt;p&gt;Multiple metrics make this stricter rather than looser. The controller computes a count per metric and takes the largest, so a scale-up on any one metric still goes through. But if a metric cannot be converted into a count at all while the metrics that do resolve suggest scaling down, the whole scale-down is skipped. A broken metrics source therefore freezes shrinkage while leaving growth possible.&lt;/p&gt;
&lt;h2 id="stabilisation-and-rate-limits"&gt;Stabilisation and rate limits&lt;/h2&gt;
&lt;p&gt;Scale-down is filtered through a 300-second stabilisation window. Every recommendation is recorded and the controller applies the highest one inside the window, approximating a rolling maximum. A brief spike that produced a recommendation of ten replicas keeps the applied count at ten until that recommendation ages out, so the first real shrink happens a full five minutes after load dropped. Scale-up has no stabilisation window at all.&lt;/p&gt;
&lt;p&gt;Rate limiting is asymmetric too. The default scale-up policy allows the larger of 100% of current replicas or four pods to be added every 15 seconds, and scale-down allows 100% of replicas to be removed. A cold HPA starting from one replica can therefore go to five, then ten, then twenty, doubling before the metrics have had time to react — a feature during a spike and a hazard when the metric is wrong.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The classic failure is a target with no denominator. The HPA reports unknown utilization, logs a condition such as &lt;code&gt;FailedGetResourceMetric&lt;/code&gt; or &lt;code&gt;ScalingActive=False&lt;/code&gt;, and never changes the replica count — so the symptom is not an error page, it is a service that keeps timing out at a fixed size. The cause is usually a container without a CPU request, or a missing metrics-server; &lt;code&gt;kubectl create deployment&lt;/code&gt; emits &lt;code&gt;resources: {}&lt;/code&gt; and has no flag to set one, so this is the default state of a hand-made manifest.&lt;/p&gt;
&lt;p&gt;The second failure is a target that cannot be reached. Because &lt;code&gt;maxReplicas&lt;/code&gt; is applied after the ratio, a workload needing more than the cap keeps computing a larger number every tick and keeps being truncated: utilization stays pegged, the status reports that it cannot scale further, and no event says the target itself is impossible. Set the cap from measured capacity, not from hope.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Confirm that a metric exists, that a denominator exists, and what the controller decided:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl get hpa web -o wide
kubectl top pods -l app=web
kubectl get deploy web -o jsonpath=&amp;#39;{.spec.template.spec.containers[0].resources.requests.cpu}{&amp;#34;\n&amp;#34;}&amp;#39;
kubectl get hpa web -o jsonpath=&amp;#39;{range .status.conditions[*]}{.type}={.status} {.reason}{&amp;#34;\n&amp;#34;}{end}&amp;#39;
kubectl describe hpa web | sed -n &amp;#39;/Events/,$p&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first line shows current against target utilization; &lt;code&gt;&amp;lt;unknown&amp;gt;&lt;/code&gt; means the metric is missing. The third prints nothing when no CPU request is set, which is the usual reason the second command works while the HPA still refuses to scale. The last two name the condition reason and any &lt;code&gt;FailedGetResourceMetric&lt;/code&gt; event, which is where the real explanation lives.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/"&gt;Horizontal Pod Autoscaling&lt;/a&gt;, the algorithm, tolerance, missing-metric handling, stabilisation window and default policies, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/"&gt;HPA walkthrough&lt;/a&gt;, the metrics-server dependency and the requests prerequisite, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kubernetes-sigs/metrics-server"&gt;metrics-server&lt;/a&gt;, the resource metrics API implementation the HPA reads, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>