Render spins your free-tier service to zero 15 minutes after the last request and cold-starts it on the next one. It works because Render is the activator — every request hits their proxy first. Kubernetes 1.36 just shipped an HPA that can also scale to zero. It does not give you the same thing.
The official Kubernetes v1.36 release blog is precise: HPA scale-to-zero remains Alpha, disabled by default, and only for workloads using Object or External metrics. No CPU. No memory. No "add minReplicas: 0 and you're done." If you run a self-hosted PaaS on a Cluster API fleet and want tenants to get genuine scale-to-zero — not just "we scaled the Deployment to zero and now nothing can wake it" — you still wire the control plane yourself. Here is what that wiring actually is, where Render's model still wins, and where owning the activator beats renting it.
The answer up front: three ways to get to zero, and what each actually gives you
| Path | What you deploy | Wake mechanism | Cold-start the tenant sees | What you operate |
|---|---|---|---|---|
| Render free-tier spin-down | Nothing — it's a billing tier | Render's edge proxy holds the request, starts the service, then forwards | ~400 ms–2 s (Node/Go) to ~5 s+ (heavy JVM), billed as zero standing compute | Nothing |
Native HPA HPAScaleToZero (K8s 1.36 Alpha) | HPA with minReplicas: 0 + one External/Object metric + HPAScaleToZero feature gate on kube-controller-manager and kube-apiserver | None by itself — if replicas are zero, no Pod exists to produce a metric or receive traffic | Broken for HTTP without an activator; traffic gets a 503 until something external scales you | HPA + a metrics source that exists when pods don't (Prometheus Adapter, Datadog, etc.) |
| KEDA HTTP Add-on or Knative Serving | KEDA ScaledObject/HTTPScaledObject + interceptor, or Knative Service + activator + Kourier/Contour | Interceptor/activator buffers the first request while KEDA/KPA scales from zero, then proxies it once Ready | Same cold-start duration, but no dropped request — caller waits, then gets a 200 | Interceptor/activator (always-on), KEDA operator or Knative control plane |
The deliverable in one sentence: native HPA can scale down to zero on an external signal, but it cannot scale up from zero on an HTTP request without a proxy that holds the request for it. Render bundles that proxy. On your own fleet, KEDA's HTTP Add-on or Knative's activator is that proxy.
What Kubernetes 1.36 actually shipped
The v1.36 "Haru" release lists HPA scale-to-zero under "New features in Alpha":
"Until now, the HorizontalPodAutoscaler required a minimum of at least one replica to remain active ... Kubernetes v1.36 continues the development of the HPA scale to zero feature (disabled by default) in Alpha, allowing workloads to scale down to zero replicas specifically when using Object or External metrics."
The history matters. HPAScaleToZero first appeared behind a feature gate in v1.16 (late 2019) and sat at Alpha for seven years. Community posts in mid-2026 claiming "1.36 enables it by default" or "graduated to Beta" contradict both the release blog and the KEP still marked Alpha; the PR that improved 1.36's behavior (#135118, adding a ScaledToZero condition) did not flip the default. If you enable it, you enable it explicitly:
# kube-controller-manager and kube-apiserver
--feature-gates=HPAScaleToZero=trueAnd the HPA itself must meet three constraints:
minReplicas: 0in the HPA spec — the only visible change to your YAML.- At least one External or Object metric. CPU and memory cannot drive a scale-from-zero because when replicas are zero there are no pods to report CPU/memory. An external signal — queue depth, requests-per-second from a gateway, a Prometheus query like
http_requests_per_second— exists independently of pods. - Deployed at one replica first. Deploy at
replicas: 1and let HPA scale down. Starting at zero breaks the bootstrap: vanilla HPA treats a zero-desired replica count as implicit maintenance mode whenminReplicaswould otherwise be positive, and at least one reported race (KEP-849 follow-up on External roles) shows zero-initialized workloads that never get their first scaling tick.
Enable the gate, meet the three constraints, and HPA will scale a Deployment to zero when the external metric stays at zero. What it will not do is bring it back on an inbound HTTP request — because there is no inbound HTTP request reaching a pod that does not exist.
Why an HTTP service needs an activator, not just an autoscaler
Draw the request path for an idle service with zero pods behind a ClusterIP Service:
Client → Ingress (nginx / Gateway API) → Service → Endpoints (empty) → 503The Ingress has no endpoint to route to. The HPA has no metric tick that says "a request arrived." The two subsystems do not rendezvous. Scaling down to zero is an HPA decision driven by a metric. Scaling up from zero on HTTP needs a component that sees the request before the workload does.
That is what the two production paths provide, and why Render's blog post "How Render Scaled Knative to Support 100k+ Free-Tier Apps" is still the best mental model: Render uses Knative behind the scenes precisely because Kubernetes "didn't natively support scale-to-zero (it still doesn't, as of September 2023)" — and the fix is a proxy in the data path.
Path A: KEDA HTTP Add-on (most PaaS teams start here)
KEDA itself scales on events — queue lag, cron, Prometheus — but its CPU/memory scalers explicitly cannot scale to zero (minReplicaCount ≥ 1). For HTTP, you add the KEDA HTTP Add-on:
Client → Ingress → KEDA Interceptor (buffers request) → KEDA scales Deployment 0→1 → Pod Ready → Interceptor forwardsMinimal wiring:
# 1. Standard Deployment — no special annotations
apiVersion: apps/v1
kind: Deployment
metadata:
name: tenant-api
spec:
replicas: 1 # let HPA/KEDA scale down, don't start at 0
template:
spec:
containers:
- name: api
image: registry.example.com/tenant-api:latest
ports: [{ containerPort: 8080 }]
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 5# 2. HTTPScaledObject — the HTTP metric source
apiVersion: http.keda.sh/v1alpha1
kind: HTTPScaledObject
metadata:
name: tenant-api
spec:
hosts: ["tenant-42.example.com"]
targetPendingRequests: 1 # scale up when 1 request is queued
scaledownPeriod: 300 # 5 min idle before 1→0
scaleTargetRef:
name: tenant-api
kind: Deployment
apiVersion: apps/v1
service: tenant-api
port: 8080What you operate: the KEDA operator, the HTTP Add-on interceptor (one Deployment, always-on per cluster or per namespace), and a metrics source for non-HTTP workloads. The interceptor is the always-on cost — a few hundred millicores that never scale to zero themselves.
Path B: Knative Serving (when you want the whole request lifecycle)
Knative's Activator is conceptually the same proxy but deeper: it replaces your Service with a Knative Route, keeps request statistics in a ring buffer, and drives the Knative Pod Autoscaler (KPA) instead of HPA. Target Burst Capacity controls whether the Activator stays in the path only during scale-from-zero (-1 means always in path).
Client → Kourier/Contour → Activator (buffers) → KPA scales 0→1 → Pod Ready → forwardTradeoff: Knative owns more of the serving path (you get revision-based rollouts and traffic splitting for free) but you route through its abstractions. KEDA scales an ordinary Deployment you already have. Most self-hosted PaaS teams that already speak Deployments + Gateway API pick KEDA HTTP Add-on; teams building a new green-field serving layer pick Knative.
Both add latency on cold start — and that latency is the real product decision.
The three gotchas that break the demo
1. Liveness probes that wake idle work
A liveness probe that checks a downstream (database, cache, another service) turns every DB blip into a fleet-wide restart storm. Worse for scale-to-zero: a probe that fails under load (GC pause, thread exhaustion) turns "slow" into "restart → colder → slower."
- Do not add a liveness probe unless "restart cures me" is literally true for that container. Most API containers should have only a readiness probe (and, for slow boots, a startup probe that replaces a giant
initialDelaySeconds). - Shutdown ordering matters. On scale-down,
SIGTERMand endpoint removal race in parallel. Serve while draining, or addpreStop: sleep 5so the endpoint propagation wins before the process exits. Otherwise scale-to-zero looks like failed requests.
2. The bootstrap problem
Deploying at replicas: 0 does not "start at zero." It starts at a state HPA treats as maintenance mode and may never leave. The working pattern from production KEDA/Knative users:
- Deploy at
replicas: 1. - Let the autoscaler observe idle metrics and scale to zero on its first down-scale window.
- From then on, the activator/interceptor drives scale-from-zero.
Expect the first cold-start wall-clock to be image-pull + Pod scheduling + app boot + readiness gate. On a warm node pool with a small Go/Node image, that is 1–3 seconds; on a cold node requiring a Cluster API scale-up, add node provisioning time. Measure it — do not assume the activator hides it.
3. Buffering is not free
The interceptor/activator holding a request is holding a connection. Under burst-from-zero, the buffer itself needs sizing: request timeout, max buffered requests, and — for WebSockets or long-poll — whether to reject scale-to-zero entirely for that route. Mark non-idempotent or latency-sensitive paths as minReplicas: 1 and let only idle-tolerant tenants scale to zero.
When to not scale to zero at all
- Stateful workloads. Postgres, Redis, any volume-backed Deployment that takes minutes to replay a WAL on cold start — keep one replica warm. Scale the stateless edge, not the state.
- Sub-100 ms SLOs. If your p95 budget is 50 ms, a 1–3 s cold start on first request is not "amortized," it's a breach. Pin those routes to one replica and use HPA's normal 1→N scaling.
- Cron/worker pools. Scale-to-zero for workers is driven by queue depth (plain KEDA
ScaledObjectonlagCount), not HTTP. Different metric, same "metric must exist when pods don't" rule — a dead-letter queue with no exporter gives you the same empty-metric problem.
The self-hosted PaaS calculus
On Render, scale-to-zero is a billing feature: your free-tier service sleeps, Render's proxy wakes it, you pay nothing while idle and pay cold-start latency when busy. On your own Hetzner fleet under Cluster API, scale-to-zero is a control-plane feature you build: feature gate, external metric, activator Deployment, probe discipline, and a placement decision about which tenants tolerate a cold start.
The upside of building it is choice Render cannot give you:
- Per-tenant policy. Render's 15-minute spin-down is global. Your
HTTPScaledObjecthasscaledownPeriod: 300for the hobby tenant andscaledownPeriod: 86400(orminReplicas: 1) for the paying one, in the same cluster. - Node economics. A fleet of 20 microservices at one replica each is 20 pods warming nodes 24/7. Scaling 15 of them to zero at night lets Cluster Autoscaler (or Karpenter) actually consolidate nodes and save the Hetzner box, not just the pod. The saving is at the machine level, not the container level.
- No per-request billing proxy. The activator's cost is flat — the interceptor's Deployment — versus a serverless product that charges per invocation. That matters once a tenant crosses from "hobby" to "steady traffic with idle nights."
The cost is operational surface: one more always-on component that must itself be highly available (if the interceptor is down, nothing wakes), one more metrics pipeline that must emit when pods are zero, and probe/shutdown math that must not wake services you just scaled down.
If that tradeoff sounds familiar, it is. Bex is built on the same premise: a self-hosted PaaS on machines you own, with a Render-compatible API so git push and bex scale feel the same — but the control plane, the activator, and the scale-to-zero policy are yours to tune, not a hosted vendor's global default. For workloads that should genuinely sleep, wire KEDA HTTP Add-on once and let tenants opt in per route. For workloads that should never cold-start, leave them at one.
What to wire tomorrow
- Enable the gate in staging only. Add
HPAScaleToZero=trueto controller-manager and apiserver in one non-production Cluster API workload cluster. Do not flip production until the app's external metric is proven. - Pick one HTTP service and one queue worker. For HTTP, install KEDA with the HTTP Add-on and create one
HTTPScaledObjectwithtargetPendingRequests: 1. For the worker, create a plainScaledObjecton queue lag withminReplicaCount: 0. - Measure three numbers: idle-to-zero time (
scaledownPeriodexpiry), zero-to-ready wall-clock (including image pull), and p95 of the first request through the interceptor. Those three numbers — not the Kubernetes version — decide whether a tenant should scale to zero. - Fix probes before you scale. Remove any liveness probe that checks a downstream, add a readiness probe that reflects actual serving readiness, and set
terminationGracePeriodSeconds+preStopso draining beats SIGKILL.
Kubernetes 1.36 did not finish scale-to-zero. It kept it possible — behind a gate, for external metrics, with a condition that lets tooling know when a workload is at zero. The rest — buffering the first request, not waking idle pods via probes, and deciding which tenants should sleep at all — is still the PaaS operator's job. That is the seam where a self-hosted platform earns its keep.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with a Render-compatible API any agent can call. If you're wiring scale-to-zero on a Cluster API fleet, star the repo on GitHub and bring your own activator — the control plane is yours.