Scale-to-zero app hosting, and what it costs you
Look at the request graph for any internal tool, staging environment, side project, or B2B app outside business hours. It's flat. Not low — flat. And yet the machine underneath it is running, billing, and patiently answering health checks all night, every night, at exactly the same rate as it does during your traffic peak.
The industry's answer is scale-to-zero: when nothing has arrived for a while, stop the machine and stop the meter; when a request finally shows up, bring it back. Simple to describe. The engineering is entirely in the two words 'bring it back', and the operational reality is entirely in what counts as a request. I build PandaStack, which does this, and both halves have bitten me.
Why idle dominates the bill
An app serving business hours in one timezone is genuinely busy for something like 40 hours of a 168-hour week. That's a 24% duty cycle, and that's a generous read — it counts every working hour as busy, which no dashboard supports. Internal tools and staging environments run at single-digit percentages.
So the always-on model charges you roughly four times what your traffic warrants, and considerably more than that for anything internal. Everybody knows this. The reason we all keep paying it is that the alternative used to be worse: a machine that takes two minutes to come back is not a machine you're willing to turn off, so you leave it running and file the cost under 'infrastructure'.
Scale-to-zero isn't a pricing feature. It's a latency feature that happens to change the bill — and it only works if waking up is faster than a user's patience.
What 'asleep' actually means
There are three implementations hiding behind the same marketing phrase, and they have very different wake characteristics.
- Pause the machine, keep it resident. RAM stays allocated on the host, the vCPUs stop. Wake is nearly instant, but you're still occupying the scarce resource, so this is only 'free' if the provider is reselling the memory — which they are not.
- Snapshot and stop. Memory and disk state are written out, the machine is destroyed, and waking restores that image. Wake is fast if the image is on local disk, slower if it has to come across the network first.
- Destroy and rebuild. Nothing is kept but configuration; waking means booting a fresh machine and, in the worst designs, rebuilding the app from source. This is the one that produces horror stories about a 'sleeping' app taking several minutes to answer.
PandaStack's app sleep is the middle one: the app's state is captured and the VM is deleted, and waking restores from that image rather than redeploying from git. I want to be plain about the numbers, because I've seen this oversold everywhere including in our own early copy. A restore from a local image is a sub-second operation. An app waking from a cold host, where the image has to be fetched across the network first, is measured in tens of seconds, not milliseconds. Those are very different user experiences and any honest platform will tell you which one you're getting.
The bug nobody predicts: your app never sleeps
Here's the failure I want to spend the most words on, because it's counterintuitive and it silently deletes the entire benefit.
Naively, the idle timer resets on every incoming request. Reasonable — a request means someone is using the app. Except almost nothing that hits a public URL overnight is a person. It's your uptime monitor every 60 seconds. It's the platform's own health checker. It's Googlebot, three security scanners, a certificate-authority validation probe, and something looking for `/wp-login.php`.
Every one of those resets a naive idle timer. The result is an app that is technically capable of sleeping and empirically never does — and the bill looks exactly like always-on hosting, because it is always-on hosting. We shipped this bug. A user's apps with custom domains never slept, and the initial suspicion landed on the domain routing, which turned out to be entirely innocent. The culprit was automated traffic resetting the timer.
The fix is a traffic classifier that separates two questions people usually conflate.
- Does this request keep the app awake? Only genuine user traffic should. Monitors, crawlers, and vulnerability scanners should not reset the idle timer.
- Does this request wake a sleeping app? Different question with a different answer. A real user hitting a sleeping app must wake it. A scanner probing for PHP admin panels at 4am must not — otherwise every bot on the internet has a button that spins up your infrastructure.
Get the second one wrong and you've built a denial-of-wallet vector: anyone who can reach your URL can force your machines to boot as fast as you can bill for them. Classification is imperfect — user agents lie, and I would rather be occasionally wrong toward 'wake it' than serve a real user an error — but the difference between classifying and not classifying is the difference between a feature and a line item.
Writing an app that survives being stopped
Scale-to-zero moves a requirement into your application that you might not have had before: the process can vanish between requests. Most web apps already tolerate this because they were written for autoscaling. Some don't.
- In-memory state is not durable. Sessions, caches, rate-limit counters, and queues in local memory survive a pause but not a rebuild. Put anything you'd miss in a database or a store outside the app.
- Background timers stop when the app does. A `setInterval` that reconciles something every ten minutes does not run while asleep, and does not catch up on wake. Real scheduled work belongs in a scheduler that exists independently, not in the request-serving process.
- Long-lived connections drop. WebSocket clients need reconnect logic — which they should have anyway, since the alternative assumption is that networks never fail.
- Wall-clock jumps forward on wake. Anything caching a timestamp at startup and comparing it to `now()` will see a discontinuity, and clock-sensitive code that has never seen a five-hour gap between two lines of execution will find creative ways to misbehave.
- The first request after wake is slow. Your own warmup — JIT, connection pools, lazy imports — stacks on top of the platform's restore time.
When you should just leave it running
Scale-to-zero is wrong for a customer-facing app with real traffic. If requests arrive every few seconds, the app never sleeps anyway and you've added a wake-latency risk for no saving. Worse, an app that idles just past the threshold during a traffic lull will sleep and then serve a cold wake to a paying customer. Being on the boundary is the worst place to be.
It's also wrong when the tail matters more than the mean. A checkout flow where 1 in 500 requests takes twenty seconds is a conversion problem, not a hosting problem, and no cost saving justifies it.
Where it's obviously right: staging and preview environments, internal admin tools, per-customer instances in a long tail where most customers are inactive most of the time, demo environments, and anything on a schedule with long gaps between runs. In those cases the duty cycle is often under 5%, and the users are people who understand that the thing they open twice a month takes a moment to warm up.
The honest summary: scale-to-zero converts a fixed cost into a variable one and a latency guarantee into a latency distribution. That's a great trade for most of what you run and a terrible trade for the most important thing you run. Decide per app, not per platform — and then go check whether your uptime monitor is holding the whole thing awake, because it probably is.
Frequently asked questions
What does scale-to-zero actually mean for a hosted app?
It means that when no genuine traffic has arrived for some idle period, the platform stops the app and stops billing for its compute, then brings it back when a real request arrives. The implementations differ substantially: pausing a machine while keeping its memory resident wakes almost instantly but still occupies host resources; snapshotting state and destroying the machine frees the resources and restores on demand; and the crudest version destroys everything and rebuilds from source, which is where multi-minute wake times come from. Ask which one you are getting, because the wake latency varies by orders of magnitude between them.
Why does my app never actually go to sleep?
Almost certainly because automated traffic is resetting the idle timer. Overnight, a public URL is hit by uptime monitors, platform health checks, search crawlers, security scanners, and bots probing for admin panels — and a naive idle timer treats every one as user activity. The app is then technically capable of sleeping and never does. The fix is classifying traffic on two separate axes: whether a request should keep an awake app awake, and whether a request should wake a sleeping one. Both need to answer no for scanners, or you have either an app that never sleeps or a URL anyone on the internet can use to force your infrastructure to boot.
How long does it take a sleeping app to wake up?
It depends entirely on where its saved state lives. Restoring from an image already on the host's local disk is a sub-second operation. If the image has to be fetched over the network first — which is what happens on a genuinely cold host — realistic wake times are in the tens of seconds. Any platform quoting only its best case is quoting the local-disk path. The number worth measuring for your own app is the first request after your longest idle stretch, typically Monday morning, since that is the one your users will actually experience.
What breaks in my application when it gets stopped and restarted?
Anything held only in process memory: sessions, caches, rate-limit counters, in-memory queues. Background timers stop while the app is asleep and do not catch up on wake, so genuinely scheduled work belongs in an external scheduler rather than a setInterval in the web process. Long-lived connections such as WebSockets drop and clients need reconnect logic. The wall clock also jumps forward across a sleep, which surprises code that caches a timestamp at startup, and on snapshot-based platforms a guest whose clock is not resynchronised on resume will reject valid TLS certificates as not-yet-valid.
When should I not use scale-to-zero?
When the app has steady customer-facing traffic. If requests arrive continuously the app never idles anyway, so you gain nothing and take on wake-latency risk — and an app that idles just past the threshold during a lull will serve a cold wake to a paying customer, which is the worst possible outcome. It is also wrong wherever tail latency matters more than average latency, such as a checkout flow. It is clearly right for staging and preview environments, internal tools, demo environments, scheduled jobs with long gaps, and per-customer instances where most customers are inactive most of the time.
49ms p50 cold start. Fork, snapshot, and scale to zero.