Fly.io machine keeps stopping: auto_stop_machines explained

Your Fly.io app worked a minute ago. Now the first request hangs for a couple of seconds, or your nightly job simply never ran.
Nothing crashed. Fly Proxy stopped your machine because it looked idle, and that is the default for new apps.
Here's why it happens, and the exact fly.toml settings for the three cases people actually have.
Why Fly.io machines keep stopping
Fly Proxy is the router in front of your app. It sees every inbound request, and it decides when machines should run.
When a region has more running machines than its traffic needs, the proxy stops or suspends one. Its stop loop runs every few minutes and stops at most one machine per region per pass.
When a request arrives and there's no spare capacity, the proxy starts a stopped machine again. It never creates or destroys machines; it only switches the ones you have on and off.
- "stop"
- default auto_stop_machines for new apps
- 0
- default min_machines_running for new apps
- ~2 s+
- Fly's figure for a common app's cold start
- few 100 ms
- resume time from a suspended machine
fly.toml auto_stop_machines, auto_start_machines, min_machines_running
Three keys under [http_service] (or [[services]]) control all of this.
auto_stop_machinesis what the proxy does to an idle machine:"off","stop"or"suspend". The oldfalseandtruemean off and stop.auto_start_machineslets the proxy start a stopped machine when a request comes in.min_machines_runningkeeps that many machines up, but only in your primary region, and only when auto-stop is on.
How to keep a Fly.io app always on
Turn auto-stop off. The proxy then never stops a machine for being idle.
[http_service] internal_port = 8080 force_https = true auto_stop_machines = "off" auto_start_machines = true min_machines_running = 1
Leaving auto_start_machines on is still useful. If a machine stops for another reason, the next request brings it back.
How to scale to zero but wake fast with suspend
Suspend is the middle ground. Fly snapshots the whole running machine, memory included, and resumes it from that snapshot.
Fly puts a resume at a few hundred milliseconds, against roughly two seconds or more for a cold start of a common app.
[http_service] internal_port = 8080 force_https = true auto_stop_machines = "suspend" auto_start_machines = true min_machines_running = 0
Suspend has conditions, all from Fly's docs:
- The machine needs 2 GB of memory or less.
- No swap configured, and no schedule configured.
- Every deploy throws the old snapshot away, so the first wake after a deploy is a full cold start.
How to stop Fly.io from stopping a background worker or cron
This is the case that fails silently. A cron schedule inside a machine that auto-stops just doesn't fire, because the machine isn't running when the time comes.
The clean fix is a separate process group with no [http_service]. Fly Proxy never touches machines it doesn't route traffic to.
[processes] web = "node server.js" worker = "node worker.js" [http_service] processes = ["web"] internal_port = 8080 auto_stop_machines = "suspend" auto_start_machines = true min_machines_running = 0 # No [http_service] for "worker": Fly Proxy never stops or starts it.
Run only one copy of the cron process. Fly's own guide warns that four machines running cron means four copies of every email.
Our own check worker runs on Fly and has to probe sites every minute, traffic or not. Its fly.toml sets auto_stop_machines = false and auto_start_machines = false, so nothing ever switches it off.
For rare jobs, Fly also has scheduled machines. They only come in hourly, daily, weekly or monthly buckets, not exact cron times.
What keeping a Fly.io machine running costs
There is no free tier for new accounts. Fly stopped selling plans on 7 October 2024; new orgs get a short trial, then pay as they go.
The trial ends after 2 machine-hours or 7 days, whichever comes first. Older plans that included free machines are still honoured for people who bought them before that date.
The smallest machine, shared-cpu-1x with 256 MB, is listed at about $2.02 a month running full time. Prices vary by region.
A stopped machine is billed only for its disk, at $0.15 per GB per month. So the real choice is a couple of dollars against a slower first request.
Fix your fly.toml with Claude or ChatGPT
Paste your config into this prompt and fill in the two blanks. It asks for the caveats most answers skip.
Here is my fly.toml. Help me fix Fly.io machines auto-stopping. [paste your fly.toml here] What the app does: [e.g. "Next.js site" / "API that receives webhooks" / "web app plus a background queue worker" / "cron jobs run inside the app"] What matters most: [fast first request / lowest bill / jobs must never be interrupted] Please: 1. Explain what my current auto_stop_machines, auto_start_machines and min_machines_running settings actually do, in plain English. 2. Remember min_machines_running only applies in the primary region, and only when auto_stop_machines is "stop" or "suspend". 3. If any cron, queue or background work runs inside a machine that has an [http_service], warn me: Fly Proxy only counts inbound requests, so it can stop that machine mid-job. 4. Suggest the smallest change that fits what matters most: "off", "suspend" (only if memory is 2 GB or less and no swap), or a separate process group with no [http_service] for the background work. 5. Give me the full corrected fly.toml and the flyctl commands to apply it.
How to know your scheduled job on Fly.io actually ran
Every fix above can quietly undo itself. A deploy resets a setting, a process group loses its machine, or the worker crashes and stays down.
An uptime check on your URL won't catch it, because the web side still answers. The job just stops, and nothing tells you.
A heartbeat flips that around. The job reports "I ran" each time it succeeds, and silence is what raises the alarm.
# In your crontab / supercronic file: only heartbeat if the job succeeded
0 3 * * * node backup.js && curl -s -X POST https://ingest.tellmewhendown.com/api/ingest/heartbeat \
-H "Authorization: Bearer $TMWD_API_KEY" \
-H "content-type: application/json" \
-d '{"type":"nightly-backup"}'The && matters: a failed backup sends nothing, so it counts as missed. There's more on the pattern in how to know if your cron job actually ran.
The same idle-sleep trap hits other hosts too. See Render spinning down, or the cross-host playbook in how to keep a free backend awake.
Every Fly setting above comes from the Fly.io autostop docs and the suspend and resume reference.
Let your Fly app sleep. Just don't let your jobs.
Join Tell Me When Down free and we'll watch your Fly.io app from outside and listen for your job heartbeats. A machine that never came back, or a cron that quietly stopped firing, and you get an email in minutes, not a message from a user.
spot something wrong or out of date? [email protected] — we'll fix it