For a decade, the Kubernetes HorizontalPodAutoscaler had a floor of one. A queue worker that processed jobs for two hours a day still kept a pod — and often the node underneath it — warm for the other twenty-two. On September 2, 2026, Johannes Würbach announced that Kubernetes v1.37 graduates HPA scale-to-zero to beta and enables it by default: an HPA on an object or external metric can now take a workload to zero replicas and back with no add-on, no alpha gate, and no HPAScaleToZero feature flag. On 1.37, minReplicas: 0 just works.
What that means in money terms depends on the workload, so here is the payoff up front before any theory:
| Workload | Before (per day) | After (per day) | What drives the remainder |
|---|---|---|---|
| Queue worker, busy 2h/day | 24 pod-hours + a warm node | ~2.5 node-hours | Busy time + cold starts + 5-min downscale stabilization + ~10-min node cooldown |
| Preview env, 3 pushes across 8h | 24 pod-hours | ~9–10 pod-hours | Awake window + stabilization tails after each push |
The worker case is close to a 90% cut in compute-hours; the preview case is roughly 60% under typical push cadence and up to 95% for a preview touched once and left alone. The rest of this post works both numbers in full, shows the exact YAML to wire it, names the two honest limits the upstream post calls out, and places core HPA against KEDA, Knative, and the platform idle-reaping logic most PaaS teams hand-rolled years ago.
Idle economics, worked in full
Start with the queue worker, because it is the workload this feature was built for. Picture a deployment consuming a task queue: genuinely busy from 09:00 to 11:00 while the nightly backlog drains, then idle until tomorrow. Before v1.37, the cheapest correct HPA setup held minReplicas: 1 around the clock — 24 pod-hours a day for 2 hours of work, plus whatever node capacity had to stay warm to host that one pod.
With minReplicas: 0 on an external queue-depth metric, the idle twenty-two hours cost nothing at the pod level. The remainder is three overheads, each small and each worth naming. First, the cold start each morning: the HPA must observe the metric cross threshold, schedule a pod, pull the image if it is not cached, and wait for the app to become ready — one operator's rule of thumb puts this at 30 seconds to 3 minutes depending on node availability and image size. Second, the default five-minute downscale stabilization window: a short dip in queue length does not immediately delete every worker, so each scale-down event carries up to five minutes of tail.
Third, at the node level, the cluster autoscaler (or any node lifecycle manager) needs its own cooldown — typically around ten minutes — before it removes the now-empty node. Add it up: 2 hours of busy time plus roughly half an hour of tails and starts lands near 2.5 node-hours a day, down from 24. The pod-level saving is automatic; the node-level saving only materializes if something actually drains and removes empty nodes, which is worth verifying rather than assuming.
Preview environments tell a similar story with wider variance, because their idle pattern is bursty rather than scheduled. Take a typical case: a developer pushes three times across an eight-hour work window, each push waking the preview, which then idles between pushes and sleeps overnight. With a wake metric tied to push activity or gateway request rate, the environment is awake roughly the eight working hours plus a stabilization tail after the last push of the day — call it 9 to 10 pod-hours against the previous 24, about a 60% cut.
The range matters more than the midpoint. A preview pushed once for a lunchtime review and then abandoned sleeps all but an hour — a ~95% cut. A preview under active iteration, with pushes every twenty minutes all day, barely sleeps at all and saves almost nothing. The honest way to forecast this is per-preview push cadence, not a fleet-wide average.
Where the savings are largest is exactly where the upstream post says: pods that reserve expensive resources. An idle worker holding dedicated CPUs is wasteful; an idle GPU inference pod is a line item. The feature's economics argument sharpens with the unit cost of the reserved resource, which is why teams running inference or batch GPU pools on owned hardware should evaluate this before teams running plain web dynos.
The metric rule: what can wake zero and what can't
There is one hard constraint, and it is structural rather than political. The HPA commonly scales on CPU or memory, but both signals come from running pods. Once the replica count reaches zero there are no pods left to measure and nothing that can tell the controller to scale back up. Object and external metrics do not have that limitation: a queue length exists independently of the workers consuming it, so the HPA can keep reading it while zero workers run.
The API server enforces this directly. An HPA with minReplicas: 0 must carry at least one object or external metric; a resource-metrics-only HPA asking for zero is rejected at admission. If you remember one sentence from this section, make it this one: CPU and memory can scale you down toward zero, but they can never wake you from it.
For queue workers and batch processors the wake signal is obvious — queue depth, stream lag, pending-task count — exposed through the External Metrics API. For preview environments it takes deliberate design, and this is where teams should slow down. A preview behind a plain Kubernetes Service has no natural wake signal: a Service does not buffer requests and exposes no queue for the HPA to read, so a zeroed HTTP preview behind a plain Service simply never wakes.
Three patterns that do work: an external metric on gateway or proxy request rate (requests arrive at the edge, the HPA reads the rate, the preview wakes); an object metric tied to PR or branch activity (a push event sets a value the HPA watches); or a cron-style window that keeps previews awake during working hours and zeroes them overnight. If none of those exists in your stack, core HPA alone cannot sleep your previews — that job stays with the platform's own idle-reaping logic or a request-buffering layer like Knative, covered below.
Wire a worker end to end
The upstream walkthrough uses a Prometheus metric named queue_consumer_lag with a name label identifying the queue, already scraped into Prometheus:
queue_consumer_lag{namespace="default",name="worker_tasks"}Kubernetes needs a metrics adapter to serve that series through the External Metrics API. The reference implementation is the Prometheus Adapter, with an externalRules entry mapping the series to an external metric:
externalRules:
- seriesQuery: '{__name__="queue_consumer_lag",name!=""}'
metricsQuery: sum(<<.Series>>{<<.LabelMatchers>>}) by (name)
resources:
overrides:
namespace:
resource: namespaceBefore touching the HPA, verify the metric is actually readable through the API. An HPA cannot scale from zero when its metric is unavailable, so this check is the load-bearing step, not a courtesy:
kubectl get --raw '/apis/external.metrics.k8s.io/v1beta1/namespaces/default/queue_consumer_lag?labelSelector=name%3Dworker_tasks'If that returns a value for worker_tasks, the pipeline is sound. The HPA itself targets a deployment named queue-worker, allows zero to ten replicas, and requests one replica per 30 queued tasks:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: queue-worker
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: queue-worker
minReplicas: 0
maxReplicas: 10
metrics:
- type: External
external:
metric:
name: queue_consumer_lag
selector:
matchLabels:
name: worker_tasks
target:
type: Value
value: "30"When the queue empties, the HPA takes the deployment to zero; when tasks arrive, the external metric is still there and the controller computes a fresh replica count capped at ten.
Three gotchas decide whether this works on day one. First, deploy at one or more replicas and let the HPA scale down naturally — manually setting a deployment to zero has always paused autoscaling, and the HPA preserves that behavior: it will not wake a workload it did not scale down itself. Second, the metric must exist before the HPA needs it; a missing metric at 3 a.m. is an outage shaped like an autoscaler. Third, the default five-minute downscale stabilization window means brief queue dips do not nuke your workers; tune it via spec.behavior.scaleDown only once you have watched the default behave.
The controller tracks ownership of the zero state with a ScaledToZero status condition: True when the HPA itself scaled the workload down (keep evaluating the metric), False with reason NotScaledToZero once it scales back up. Inspect it with kubectl describe hpa queue-worker — a workload sitting at zero without ScaledToZero=True is a manual pause, not an autoscaled idle, and nothing will wake it.
One upgrade warning belongs in a box, not a paragraph: the HPAScaleToZero gate must be enabled on both kube-apiserver and kube-controller-manager (it is by default in 1.37). During a version-skewed control-plane upgrade, wait until both components support the feature before creating zero-capable HPAs — a controller without it treats replicas: 0 as a manual pause and may strand a workload at zero. Before any downgrade or gate disable, move affected HPAs back to minReplicas: 1 and manually scale zeroed workloads up first.
The two honest limits
The upstream post names both limits plainly, and both should shape where you adopt this first. The first is cold-start time. Waking from zero is a sequence — the HPA observes the metric, schedules a pod, the kubelet pulls and starts the container, the app becomes ready — and that sequence takes tens of seconds at best, minutes at worst.
This is a non-issue when work waits in a durable queue: a task sitting in RabbitMQ or SQS for an extra minute is invisible. It is a dealbreaker for latency-sensitive interactive traffic, where the first request after idle eats the entire wake sequence. Match the workload to the tolerance before matching the HPA to the workload.
The second limit is that Kubernetes Services do not buffer requests while no pods are ready. An HTTP request arriving at a zeroed Service has nowhere to wait; it fails rather than queuing. So request-driven HTTP workloads need a separate buffering layer in front of the zero-capable deployment — Knative Serving with its activator, the KEDA HTTP add-on with its interceptor, or a gateway that holds requests during scale-from-zero. Core HPA gives you the scaling primitive; something else must hold the request while the pod starts. Budget for that component explicitly instead of discovering its absence from 503s.
A third failure mode deserves a sentence even though it is operational rather than architectural: if the adapter stops serving the metric, the HPA reports ScalingActive=False with a reason like FailedGetExternalMetric and holds its last replica count. A worker that was awake stays awake; a worker at zero stays at zero until someone restores the metric or scales manually. Monitor the metric pipeline with the same severity as the workload, because from the HPA's perspective they are the same thing.
Where this leaves KEDA, Knative, and your idle-reaper
The "do you still need KEDA" takes arrived within days of the beta announcement, and the fair answer is narrower than the headlines: core HPA now covers the single-metric queue-consumer case that previously required either KEDA or the alpha gate, but KEDA was never just scale-to-zero. A KEDA ScaledObject generates and manages an HPA under the hood while adding 60-plus event-source scalers — Kafka, SQS, Redis, cron, Prometheus queries, cloud queues — plus the 0-to-1 activation logic. If your fleet scales on one Prometheus metric, native HPA is now the simpler answer with one fewer operator to run. If it scales on Kafka lag plus a cron window plus an SQS queue, KEDA still earns its place. One practical note: KEDA and the Prometheus Adapter both want to serve the cluster-wide external.metrics.k8s.io APIService and cannot both own it, so mixed fleets should plan which component serves external metrics before installing both.
Knative sits on a different axis. Its value was never the autoscaling arithmetic but the request path: the activator buffers HTTP requests during scale-from-zero so the first request after idle waits instead of failing. Nothing in the v1.37 beta changes that — core HPA plus a plain Service still drops the first request. Teams running HTTP services with true scale-to-zero keep Knative (or an equivalent buffering proxy); teams whose idle workloads are all queue- and batch-shaped can now skip it.
The decision table for a platform team looks like this:
| Workload shape | Reach for | Why |
|---|---|---|
| Queue consumer / batch processor on one metric | Core HPA, minReplicas: 0 | No add-on, no gate, metric-first and done |
| Event-driven across many sources, cron, cloud queues | KEDA ScaledObject | 60-plus scalers plus activation logic HPA will never ship |
| HTTP service needing request survival at zero | Knative / KEDA HTTP add-on / buffering gateway | Only these hold the request while pods start |
| Preview env with a wake metric (gateway RPS, PR activity) | Core HPA on that metric | Same primitive as workers once the signal exists |
| Preview env with no wake metric | Platform idle-reaping | HPA cannot wake what it cannot observe |
Adoption order follows from the table: put workers and batch processors on core HPA first, where the metric already exists and the failure modes are forgiving. Keep the HTTP path exactly as it is until a buffering layer is in place. Treat preview sleep as a per-signal project — wire the wake metric, then the HPA — rather than a flag flip. And run the metric-first rollout rule everywhere: adapter, verification query, then HPA, in that order, per workload.
The floor is gone; the metric is the job
HPA scale-to-zero took a long road to beta — the first alpha shipped back in v1.16, and v1.36 did the careful redesign work with the ScaledToZero condition that distinguishes an autoscaled idle from a deliberate pause. What v1.37 changes is the default: the feature is on, the API accepts minReplicas: 0, and the only remaining question is whether your workload has a signal that survives at zero. Upstream is explicit that beta is a feedback-gathering phase before any GA conversation, so early adopters should bring SIG Autoscaling their operational notes — stabilization tuning, cold-start distributions, adapter behavior — via the #sig-autoscaling channel.
For a self-hosted PaaS on owned hardware, the takeaway is concrete: the idle pod tax that every queue worker and batch processor paid for years is now optional, priced at one external metric per workload. Start with the workers, name the wake signal for everything else, and let the always-on replica become the exception you can justify rather than the default you never questioned.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



