Skip to main content

Kubernetes 1.36's HPA Can Now Scale to Zero: What It Actually Changes for Idle-App Billing

9 min readDora NodaDora Noda
Share
On this page

HPAScaleToZero sat behind a feature gate, in alpha, for seven years. It landed in Kubernetes 1.16 back in 2019, and nothing about it changed until Kubernetes v1.36 "Haru" shipped on April 22, 2026 and flipped it on by default. That's the entire news item. Here's the part that actually matters to anyone running a fleet of tenant apps: what changes the day you no longer need a separate add-on just to let an idle Deployment cost nothing.


What Actually Changed, in One Table

CapabilityPre-1.36 (plain HPA)Kubernetes 1.36 nativeStill needs an add-on
Scale a Deployment down to 0 replicas when load hits 0Required the HPAScaleToZero alpha gate manually enabled — most clusters never turned it on, so minReplicas was floor-1 in practiceOn by default. Set minReplicas: 0 on a plain resource-metric HPA and it works, no gate flagNo
Scale a Deployment back up from 0 replicas when a request arrivesNot possible with plain HPA at any settingStill not possible with plain HPAYes — something has to emit a "there is demand" signal from outside the Deployment, because a CPU/memory metric has nothing to sample when 0 pods are running
Stop paying for CPU/memory reservation while idleOnly via KEDA managing a shadow HPA underneath a ScaledObjectNative, zero third-party controllersNo
Detect the first request to a cold app and hold it until a pod is readyKEDA's HTTP add-on interceptor (a proxy that queues the request in front of the Deployment)Not part of core KubernetesYes

The one-line version: Kubernetes 1.36 removes the KEDA dependency for the down half of scale-to-zero. It does nothing for the up half. Those are two separate problems, and conflating them is the single most common misreading of this release showing up in blog posts right now.


Why "Scale Down" and "Scale Up From Zero" Are Different Problems

A resource-based HPA metric — CPU utilization, memory utilization — is a measurement taken from running pods. When a Deployment's traffic drops to zero and its pods sit idle, CPU usage approaches zero too, and the HPA controller can act on that: it sees "0-ish," it scales toward minReplicas, and now that minReplicas: 0 is a supported, default-on setting, it can go all the way down. That direction was always mechanically simple; it just needed the floor removed.

Scaling back up is not the mirror image of that. Once a Deployment is at 0 replicas, there is no pod left to report a CPU or memory number — the metric doesn't read "0," it doesn't exist. Nothing in the resource-metrics path can tell the HPA controller "traffic just arrived," because the thing that would normally emit that signal is exactly the thing that got scaled away. This is not an oversight KEP-2021 left unfixed; it's a structural gap in what a resource metric can observe at zero replicas, and native Kubernetes 1.36 doesn't change the shape of that gap at all.

KEDA's HTTP add-on solves it with a component that sits outside the Deployment: an interceptor proxy in the request path that counts and queues incoming HTTP requests, reports "request pending" as an external metric HPA can act on, and holds the first request in a buffer until a cold pod becomes ready to serve it. That's still the only production-shipping way to get the "up from zero" half working, HTTP or otherwise — and it means KEDA isn't obsolete after this release. What's obsolete is needing KEDA's shadow-HPA machinery just to unlock the minReplicas: 0 semantics on the down side. The up-side trigger is a separate piece of architecture a platform still has to run, whether that's KEDA's http-add-on or an equivalent built in-house.


The Cold-Start Budget Other PaaS Platforms Already Committed To

Before designing dormant-app pricing around this, it helps to know what latency budget existing platforms already treat as acceptable, since that's the bar a self-hosted alternative is competing against:

PlatformIdle triggerFirst-request cold-start latencyBilling while idle
Render (free tier)15 minutes with no traffic30–60 seconds$0
Fly.io Machines (auto_stop_machines)Configurable, request-driven300ms–2s$0 (compute only; volumes still billed)
RailwayNo scale-to-zero offeredN/A — stays warmBilled continuously on paid plans

That's roughly a 100x spread in cold-start latency between Render's free tier and Fly.io's Machines model, for the same underlying idea of "don't pay while nobody's asking." The gap is almost entirely about what's doing the waking-up: Render's free tier appears to rely on a slower, more generic wake path, while Fly.io's Machines are purpose-built VMs that skip a full container-image pull and scheduling cycle on the common case. A Kubernetes-native implementation using an HTTP-interceptor trigger in front of a pre-pulled image sits architecturally closer to the Fly.io end of that range than the Render end — the interceptor buffering pattern is the same shape as what Fly.io's proxy does, just implemented as a Kubernetes external-metrics source instead of a proprietary edge proxy.


What It's Actually Worth: A Worked Example

Take a representative — not real-customer, but realistic — fleet: 1,000 tenant apps running on Hetzner Cloud CPX31 nodes (4 vCPU, 8 GiB RAM, $24.99/month at 2026 pricing after Hetzner's April and June rate adjustments). Give each tenant app a typical small-service footprint: a 250m CPU / 512Mi memory request. That bin-packs cleanly at 16 tenants per node by either dimension (4,000m ÷ 250m = 16; 8,192Mi ÷ 512Mi = 16).

Without scale-to-zero, every one of the 1,000 tenants holds a minReplicas: 1 reservation whether it's serving traffic or not: 1,000 ÷ 16 = 62.5, rounded up to 63 nodes, at $24.99 each — ≈$1,574/month just to hold the reservation floor, before counting a single request served.

With native minReplicas: 0 applied to the long-tail slice of tenants that see traffic only occasionally — illustratively, 40% of a typical PaaS's free/hobby-tier apps, modeled on the same idle-then-sleep pattern Render and Heroku's free tiers have documented for years — 400 tenants no longer hold a standing reservation. The remaining 600 active tenants still need: 600 ÷ 16 = 37.5, rounded up to 38 nodes, at $24.99 — ≈$950/month.

That's 63 nodes down to 38, a reduction of 25 nodes and roughly $624/month, or about 40% of the fleet's reservation footprint — freed either as a straight cost cut or as headroom to onboard another ~400 active tenants on hardware the platform already owns, without buying a single additional node. The shared interceptor component sitting in front of the dormant tenants runs as a handful of instances serving the whole dormant pool, not one per tenant, so it doesn't meaningfully erode that savings.

This is a model to reason with, not a specific bill — the real number for any given fleet depends on actual per-tenant sizing and the true dormant fraction, which is exactly why a platform should be able to compute this from its own tenant metrics rather than take it on faith.


The Architecture a Git-Push PaaS Actually Needs

Putting minReplicas: 0 into a self-hosted, Cluster API-provisioned platform's dormant-app story means wiring together four pieces, only one of which is new in 1.36:

  • minReplicas: 0 on the tenant Deployment's HPA — new in 1.36, no add-on required. This is the piece that used to require vendoring KEDA.
  • A dormant status condition on the platform's own App CRD, flipped when the HPA reports 0 replicas, so the control plane (and the billing meter) has a first-class signal instead of inferring app state from replica counts scattered across the cluster.
  • An interceptor in the tenant's ingress path that detects the first request to a dormant app, holds it in a buffer, and reports "demand present" as an external metric — either KEDA's HTTP add-on directly, or a purpose-built equivalent sized for the platform's own routing layer.
  • A billing meter keyed to the dormant condition, not to replica count directly — zeroing out the reservation charge the instant an app goes dormant, and re-arming it the moment the interceptor wakes it back up.

None of this is exotic engineering. The point of highlighting it is narrower: 1.36 removed exactly one dependency (KEDA-for-the-down-half) from that list, and it's worth being precise about which one, so a platform team doesn't spend a sprint ripping out KEDA's HTTP add-on under the mistaken belief that stock Kubernetes now covers the whole loop.


The Honest Scope of This Release

Kubernetes 1.36 does not kill KEDA. KEDA still owns the "wake up from zero" trigger for any workload — HTTP or otherwise — and still owns the other 60-plus event sources (queue depth, cron schedules, cloud-provider metrics) that a resource-based HPA was never going to observe on its own. What changed is narrower and still genuinely useful: a self-hosted PaaS can now build the cost half of dormant-app billing — the part where an idle tenant's reservation actually drops to zero — on stock Kubernetes, and reserve the KEDA-or-equivalent dependency for the one job it's still uniquely needed for: catching the first request back.

For a platform whose entire pitch is running on machines you own instead of a vendor's idle-markup, that's a real, if incremental, win — one less controller to run for half the problem, and a much clearer architecture diagram for the half that's left.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with per-tenant scale-to-zero economics built on the same Cluster API fleet math this post walks through, not a vendor's idle-tier markup. Star the repo on GitHub or deploy your first app today.


Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex