Why scaling to zero is hard in plain Kubernetes
Kubernetes ships with the Horizontal Pod Autoscaler (HPA), which scales Deployments based on CPU or memory. But HPA has a floor it can't cross: its minimum is 1 replica. It scales *down* to 1, never to 0. That means an idle service still burns a pod's worth of CPU and memory around the clock.
For production services handling constant traffic that's fine. For the long tail — dev environments, preview deploys, internal tools, hobby projects, services with bursty or rare traffic — running 24/7 for traffic that arrives twice a day is pure waste. Scale-to-zero fixes that: zero replicas when idle, spin up on demand. That's where [KEDA](https://keda.sh/) comes in.
What KEDA is
KEDA (Kubernetes Event-Driven Autoscaling) is a CNCF project that extends Kubernetes autoscaling in two key ways:
- 1It scales on events, not just CPU/memory. Queue depth, HTTP request rate, Kafka lag, cron schedules, database query results — 60+ built-in *scalers*.
- 2It can scale to and from zero. Unlike HPA, KEDA's minimum is 0.
Under the hood KEDA doesn't replace HPA — it *drives* it. KEDA's operator watches your event source and, crucially, manages the 1↔0 transition that HPA can't, while delegating 1↔N scaling to a managed HPA it creates.
The scale-to-zero mechanism
The interesting part is how a service at zero replicas comes back. If there are no pods, who handles the first request?
The pattern (used by KEDA's HTTP add-on and similar designs) is an activator/interceptor sitting in the request path:
Request arrives
-> interceptor sees target has 0 replicas
-> interceptor holds the request, signals KEDA to scale up
-> KEDA scales Deployment 0 -> 1
-> new pod becomes Ready
-> interceptor forwards the held request to the pod
-> subsequent requests go straight throughThe first request after idle pays the cold-start cost — pod scheduling, image pull (if not cached), container start, app boot. After that, the service is warm and behaves normally until it goes idle again and scales back to zero.
A basic ScaledObject for HTTP-style scaling looks like:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: my-app
spec:
scaleTargetRef:
name: my-app # the Deployment
minReplicaCount: 0 # the magic: zero when idle
maxReplicaCount: 10
cooldownPeriod: 300 # wait 5 min of idle before scaling to zero
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
query: sum(rate(http_requests_total{app="my-app"}[1m]))
threshold: "1"The cooldownPeriod matters: scale down too eagerly and you cold-start constantly; too slowly and you waste resources. 5 minutes is a reasonable default.
KEDA vs HPA at a glance
| Capability | HPA | KEDA |
|---|---|---|
| Minimum replicas | 1 | 0 |
| Scale on CPU/memory | Yes | Yes (wraps HPA) |
| Scale on events (queue, HTTP, cron, Kafka…) | No | Yes (60+ scalers) |
| Scale-to-zero | No | Yes |
| First-request handling | N/A | Activator holds request |
The trade-offs
Scale-to-zero is a genuine win, but it's a trade, not a free lunch:
- Cold-start latency. The first request after idle is slow. For a user-facing service this is the cost you're accepting in exchange for not paying for idle compute. (We cover reducing cold starts in a dedicated post — image size, warm pools, lighter runtimes.)
- Not for everything. A service with steady traffic never goes idle, so scale-to-zero gives you nothing and adds an interceptor hop. Use it for bursty/intermittent workloads.
- Stateful gotchas. In-memory caches and warm connection pools die when the pod scales to zero. Keep state external (Redis, the database) so cold starts are stateless.
- Connection handling. Long-lived connections (WebSockets) and scale-to-zero don't mix well; the service can't go idle while a connection is open, and reconnection logic must tolerate cold starts.
Where this fits on a platform
Scale-to-zero is the economic engine behind a generous free tier. If free-tier apps ran 24/7, the math wouldn't work; scaling idle apps to zero is what makes "5 web services for $0" sustainable.
That's exactly how PandaStack uses it. Free-tier apps use KEDA scale-to-zero on spot/preemptible nodes inside a gVisor sandbox — idle apps cost nothing to run, and the first request after idle pays a cold start before the app serves normally. It's the honest trade we make explicit in our limits: free-tier apps cold-start on preemptible nodes. For production traffic you'd typically run on a tier without scale-to-zero so there's always a warm replica.
When to reach for it
Use KEDA scale-to-zero for dev/staging environments, preview deploys, internal tools, scheduled jobs, queue consumers, and any workload whose traffic is bursty enough that idle time dominates. Keep steady-traffic production services on a normal min-replica HPA. And whatever you scale to zero, design it stateless so the cold start is a latency event, not a correctness one.
References
- [KEDA — official documentation](https://keda.sh/docs/)
- [KEDA — HTTP Add-on (scale-to-zero for HTTP)](https://github.com/kedacore/http-add-on)
- [KEDA — Scalers catalog](https://keda.sh/docs/latest/scalers/)
- [Kubernetes — Horizontal Pod Autoscaler](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/)
Want scale-to-zero without writing a single ScaledObject? PandaStack's free tier runs apps with KEDA scale-to-zero automatically — try it: [dashboard.pandastack.io](https://dashboard.pandastack.io)