On August 26, 2026, Kubernetes 1.37 ("Garhwal") flipped a switch that has been sitting behind a feature gate since 2019: the HorizontalPodAutoscaler's scale-to-zero graduated from alpha to beta, and the HPAScaleToZero gate is now enabled by default. A week later, the September 2 Kubernetes blog post documented the full contract. For the first time, minReplicas: 0 just works on a stock cluster — no controller-manager flags your managed provider won't let you set.
If you run a self-hosted PaaS, that sentence should rearrange your roadmap. The idle tenant — the staging app nobody opened this week, the queue worker waiting on an empty queue, the GPU inference pod sitting warm between demos — is the line item your bill is mostly made of. Native scale-to-zero promises to take it to zero without bolting on KEDA or a full Knative serving layer.
So here is the title question, answered in the first screen:
| Workload | Native HPA minReplicas: 0 in 1.37 | Verdict |
|---|---|---|
| HTTP service, request-driven | Cannot wake on requests; a zero-replica Service refuses connections until the metric fires | Still needs Knative (or a wake proxy) for request-driven idle |
| Queue worker, metric-driven | Queue-depth external metric scales 0 → N and back; this is the happy path | Drop KEDA for this case — native HPA is enough |
| GPU inference / batch, idle-long | Same mechanism, biggest savings per pod; cold start is image-pull plus model load | Native HPA works, but budget the wake latency honestly |
The rest of this post is the evidence behind that table: what 1.37 actually changed, the twenty lines of YAML that work, the two things native HPA still cannot do, the per-tenant plumbing bill before minReplicas: 0 is a promise instead of a flag, and a worked cost comparison across all three footprints.
What 1.37 actually changed
Scale-to-zero for the HPA is not new. KEP-2021 proposed it years ago, and the HPAScaleToZero gate has existed since Kubernetes 1.16. What changed in 1.37, per the release notes and the September 2 blog post, is the maturity and the default:
- The feature graduated alpha → beta, which means the API shape is now stable.
- The gate is enabled by default on both
kube-apiserver(which validatesminReplicas: 0) andkube-controller-manager(which executes the scale). On 1.37, you set the field and it works. - The HPA now records a
ScaledToZerostatus condition (True/False), so "the autoscaler deliberately parked this at zero" is distinguishable from "someone rankubectl scaleat 2 AM." Alerting onreplicas: 0finally has a signal instead of a guess. - The full loop works in both directions: the HPA parks the workload at zero when the metric says idle, and scales back up when the metric crosses threshold — no manual intervention, no cron job re-inflating replicas.
There is one hard constraint, and it is the one that shapes everything else: minReplicas: 0 is only accepted with Object or External metrics. Try it with a type: Resource (CPU/memory) metric and the apiserver rejects it with a validation error — loudly, which is the kinder failure mode. The reason is physical, not bureaucratic: CPU and memory are measured from running pods, and at zero replicas there is no running pod left to measure. The metric that wakes the workload has to live outside the workload — a queue depth, a Prometheus query result, an external system's gauge.
This shipped in the same release as gang scheduling's beta for grouped AI/ML pod placement. The two features attack idle cost from opposite ends: gang scheduling controls how a batch of pods starts together; scale-to-zero controls whether anything is running at all when there is nothing to do.
The 20-line version that works
Here is the happy path: a deployment consuming from a queue, scaled on an external metric exposed through the metrics API (typically Prometheus plus prometheus-adapter — more on that plumbing below).
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-queue-worker
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-queue-worker
minReplicas: 0
maxReplicas: 20
metrics:
- type: External
external:
metric:
name: queue_messages_ready
selector:
matchLabels:
queue: orders
target:
type: AverageValue
averageValue: 10When the queue drains, the HPA walks replicas down to zero. When messages arrive, the metric crosses threshold and pods come back. Verify the parked state explicitly — do not infer it from the deployment:
kubectl get hpa order-queue-worker \
-o jsonpath='{.status.conditions[?(@.type=="ScaledToZero")]}'Look for status: "True". That condition is the difference between "intentionally idle" and "broken at zero," and your tenant-facing status surface should read it rather than counting pods.
One caveat before you extrapolate: this YAML shape does not transfer to HTTP services. A queue worker wakes on queue depth, which exists independently of the pods. An HTTP service's natural signal — incoming requests — has nowhere to land when zero pods back the Service: the request fails instead of queueing. Native HPA gives you no request buffer, no held connection, no "wake on SYN."
That gap is exactly what the next section is about, and it is why the decision table's first row still says Knative.
What native HPA still can't do
Two gaps, each with a concrete failure mode.
No request path. Knative Serving's autoscaler (KPA) scales on request concurrency, and its activator sits in the data path: when a revision is at zero, the activator holds incoming requests, signals a scale-up, and forwards them once a pod is ready. Cold start is slow, but the request survives. With native HPA at zero, there is no activator — traffic to the Service gets connection refused (or a 503 from the ingress, depending on your stack) until the external metric fires and a pod passes readiness.
For a PaaS tenant, "my app 503s for 30 seconds after every idle period" is not scale-to-zero — it is an outage with a fancy cause. If your idle-tenant story must wake on plain HTTP requests, you still need Knative's request-driven path or a wake proxy of your own; minReplicas: 0 alone does not give you one.
No event-source polling. KEDA's value was never just "scale to zero" — it is the catalog: dozens of built-in scalers that poll Kafka lag, SQS depth, Postgres row counts, Redis list length, cron schedules, and Prometheus queries, each turning an event source into a metric the HPA (which KEDA drives under the hood via a generated keda-hpa-* object) can act on. Native HPA replaces exactly one slice of that: the case where your metric already flows through the Kubernetes metrics APIs. Every new event source beyond that is hand-wiring per tenant — an exporter, a Prometheus rule, an adapter mapping — instead of a ScaledObject with a trigger type. KEDA remains the right call the moment your tenants' wake signals are heterogeneous event sources rather than one Prometheus query shape you control.
In short: 1.37 commoditizes the mechanism (park at zero, wake on metric) but not the plumbing on either side — not the metric production, and not the request holding.
The per-tenant plumbing bill
Before minReplicas: 0 is a promise instead of a flag on your platform, each tenant's wake path needs all of the following. This is the part that does not fit in a release announcement:
- A metrics server for external metrics. Out of the box, a cluster has no external metrics API. The standard shape is Prometheus plus prometheus-adapter, registering
external.metrics.k8s.iosoqueue_messages_ready(or your per-tenant equivalent) is queryable. Verify per tenant withkubectl get --raw /apis/external.metrics.k8s.io/v1beta1/...before you promise anything — an HPA whose metric is missing does not scale to zero safely, it just stops making decisions. - A per-tenant metric with a stable name. The HPA spec above hardcodes
queue: orders. On a PaaS, that selector is per tenant, per workload — your control plane must mint the metric, the adapter rule that exposes it, and the HPA referencing it as one atomic unit, or tenants get each other's wake signals. Metric-name namespacing is now a correctness property, not hygiene. - A scale-up latency budget, measured. The HPA polls metrics on its sync loop (default 15 seconds), then the pod must schedule, pull its image, and pass readiness. For a small HTTP-shaped worker that is typically 20–60 seconds from metric-crossing to serving; for GPU inference with a multi-gigabyte model load, minutes. Publish the number per workload class and design the tenant contract around it — a queue worker tolerates a minute; an interactive demo does not.
- A
ScaledToZero-aware status surface. Tenants will see zero pods and file tickets. Your dashboard should render the HPA condition ("idle — will wake on demand") rather than a replica count, and your alerts must treatScaledToZero=Truewith zero pods as healthy.
None of this is exotic. But it is real per-tenant control-plane work, and it is the actual cost of the feature — the YAML is the easy ten percent.
What an idle tenant actually costs at zero
Zero pods is not zero cost. Here is the honest comparison across the three footprints from the decision table, assuming a Hetzner-class node pool where a shared vCPU runs roughly €5/month and a gigabyte of RAM roughly €2/month when amortized across a packed node (your numbers will differ; the shape will not):
| Footprint | Standing cost (1 replica, always on) | At-zero cost (HPA parked) | Residuals that never go away |
|---|---|---|---|
| Small HTTP service (0.25 CPU / 512 MB) | ~€2.25/mo per tenant | ~€0.15/mo (metric series + HPA object churn) | Metrics storage, image pulls on wake, readiness-probe traffic |
| Queue worker (0.5 CPU / 1 GB) | ~€4.50/mo per tenant | ~€0.15/mo | Same, plus queue-lag monitoring and dead-letter retention |
| GPU inference slice (shared card + 4 GB) | ~€40–80/mo of reserved accelerator | ~€1–2/mo (device metric + scheduler state) | GPU node still runs (bin-pack or power down separately); model weights re-load on every wake |
Two things to notice. First, the savings scale with the footprint: parking a small HTTP replica saves a couple of euros a month per tenant — real money at a thousand tenants (roughly €2,000/month), but not the headline. The headline is the GPU row, where one parked inference workload saves more than twenty parked web apps. Prioritize scale-to-zero by footprint size, not by tenant count.
Second, the residuals are small but nonzero, and one of them bites: cold-start image pulls. Every wake re-pulls (or at least re-verifies) the workload image on whatever node takes the pod, and for GPU workloads it also means model weights loading into VRAM. At high wake frequency — a "mostly idle" tenant that actually wakes every few minutes — the pull/ready churn can cost more in node pressure and tail latency than the parked replica ever cost in RAM. Measure wakes per day per tenant; below a handful, zero wins outright, and above a few dozen, you are paying for elegance with p99s.
Sensitivity check, because one table row is never the whole story: if your nodes are half the price (reserved capacity, cheaper region), halve the standing column — the at-zero column barely moves, since metric storage dominates it. If your tenants' idle ratio is 50% rather than 95%, halve the savings again. The breakeven math stays favorable for anything idle more than roughly two-thirds of the time, which describes most staging environments, side projects, and demo tenants on any PaaS.
The verdict for a self-hosted PaaS
Kubernetes 1.37 moves scale-to-zero from "project" to "configuration" for exactly one workload shape: anything whose wake signal is a metric that exists without running pods. That shape covers queue workers, batch consumers, cron-driven jobs, and idle GPU inference — which, on most self-hosted platforms, is where the idle money actually is.
The practical policy:
- Default to native HPA for metric-wakeable workloads. One fewer operator to run, one fewer CRD family to version, and the
ScaledToZerocondition gives you the observability KEDA's generated HPAs always made awkward. - Keep KEDA where tenants bring heterogeneous event sources — Kafka here, SQS there, a database table somewhere else. Its scaler catalog is still the cheapest way to turn "that thing over there" into a wake signal.
- Keep Knative (or build the wake proxy) only where the wake signal is an inbound HTTP request. Nothing in 1.37 holds a request for a pod that does not exist yet.
The deeper lesson is about where platform value lives now. The mechanism — park at zero, wake on signal — is upstream and free. What is left for your platform to own is the per-tenant plumbing from the previous sections: metric namespacing, wake-latency budgets, and a status surface that says "idle" instead of "down." That is unglamorous control-plane work, and it is exactly the work a tenant cannot do for themselves — which makes it the product.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Idle-tenant scale-to-zero is on the roadmap as native HPA policy, not another operator to babysit. Star the repo on GitHub or deploy your first app today.



