<!-- cmdz — Scaling to zero and back: how cold starts work. Source: https://www.cmdz.com/blog/scaling-to-zero-and-back -->
# Scaling to zero and back: how cold starts work
> What actually happens between a request arriving at a sleeping app and it being served, where the milliseconds go, and when scale-to-zero is the wrong choice.

What actually happens between a request arriving at a sleeping app and it being served, where the milliseconds go, and when scale-to-zero is the wrong choice.

15 Apr 2026 · 4 min read · cmdz

A sleeping workload costs nothing. Waking one costs a moment. This post is about exactly how long that moment is, what it is made of, and when you should refuse the trade.

## What "asleep" means here

A micro-VM that has been idle past its threshold is stopped, not deleted. The image stays on the node. The volume stays attached. The DNS record and the certificate are untouched.

What stops is the compute meter, which is the entire point.

## The wake path

A request arrives at the edge for a sleeping workload:

```
edge receives request           0 ms
route lookup: workload asleep   1 ms
request held; wake signal       2 ms
micro-VM boots                 90 ms
process starts             200–2000 ms   ← your app
startup probe answers          +0 ms
request released to app
```

The micro-VM boot is about ninety milliseconds and it is not the variable. **Your process start is the variable**, and it varies by two orders of magnitude depending on what you wrote.

The request is held, not rejected. The visitor sees a slow page, never an error.

## Where your milliseconds go

Rough figures from real applications here:

| | Process start |
|---|---|
| Go binary | 15–40 ms |
| Static file server | 20 ms |
| Node, small Express app | 120–300 ms |
| Next.js production server | 400–900 ms |
| Laravel with Octane | 300–600 ms |
| Laravel without Octane | 600–1200 ms |
| Python with a large ML import | 2000–8000 ms |

That last row is where scale-to-zero stops being free. If you import torch at module level, your cold start is dominated by an import, and no amount of platform tuning helps.

## Making cold starts smaller

**Import lazily.** Move expensive imports inside the function that needs them. A health check that returns `{"ok": true}` without importing your model turns an eight-second cold start into a two-hundred-millisecond one for everything that is not an inference request.

**Do not connect to the database at startup.** Connect on first use, with a pool that fills lazily. An app that blocks its boot on a database handshake has added the database's latency to every cold start.

**Do not run migrations at startup.** They belong in the release command, which runs once per deploy. An app that migrates on boot migrates on every wake.

**Keep the image small.** Not because we charge for it, but because a smaller image is a smaller page cache to warm.

## When not to use it

Three cases where always-on is correct:

**Production apps with real users.** The first visitor of every quiet period pays the cold start. If that is a customer, you are optimising your infrastructure bill by spending their patience.

**Anything with a hard latency budget.** A webhook receiver with a three-second timeout on the sender's side will eventually miss one.

**Workloads with expensive warm-up.** If your app spends four seconds building an in-memory index at boot, sleeping throws that away every time.

For those, use a **schedule** instead of an idle threshold:

```toml
[apps.web.sleep]
mode  = "schedule"
awake = "Mon-Fri 07:00-20:00 Europe/Amsterdam"
```

Awake for the hours people use it, asleep the rest. A business application that nobody touches at night bills about 40% of a month, and no user ever meets a cold start.

## Where it is obviously right

**Preview environments.** This is the case the feature exists for. Twenty pull request previews, each genuinely used for an hour a day, cost roughly what one always-on instance costs. Ration them and you have optimised the wrong thing.

**Staging and demo environments.** Awake when someone looks at them.

**Internal tools.** The admin panel three people use twice a week does not need to run for 720 hours a month.

**Batch workers.** Scale to zero between runs, up to four during them. A worker that runs two hours a week costs € 0.40 instead of € 14.

## Interaction with the limit

Sleeping is one of the strongest levers you have for staying under a ceiling, and it is the one people reach for last.

If you are at 80% of your limit on the twentieth of the month, putting staging and previews to sleep usually buys back enough headroom to finish the month without pausing production. The usage screen breaks cost down per environment, so you can see exactly which ones to put to sleep rather than guessing.

Your agent can do this too — `cmdz_pause_app` is in the `build` profile, and "we are at 80%, pause everything that is not production" is a reasonable instruction to give something that has read your usage breakdown.

## Set your limit and start.

One click with a passkey, then you verify a payment method once to start your 14-day free trial (€ 10 of credit). After that it is prepaid pay-as-you-go — you only ever spend credit you have already bought, and no invoice ever arrives above the amount you set.

- [Create account](https://app.cmdz.com/signup)
- [Read the docs](https://docs.cmdz.com)
