auto_stop_machines decides what to stop, why a background task is invisible to that decision, and the two patterns that keep work from getting killed.
If you’re picking a queue technology or a cron runner, start with the work queues or task scheduling guides instead. This page is about the machine behavior underneath them.
The problem
A typical setup: a FastAPI endpoint accepts a request, spawns an async task to generate a report, returns202 Accepted, and closes the connection. The proxy sees no active connections. A few minutes later, it stops the machine. The report dies half-finished.
This isn’t a bug. It’s auto_stop_machines working exactly as documented. The proxy looks at inbound traffic. It does not look inside the container. From the proxy’s point of view, a machine running a 20-minute job and a machine doing nothing look identical.
There are two ways to fix it. Pick one based on whether your work is bursty or steady, request-triggered or queue-driven.
How autostop actually decides
The Fly proxy evaluates machines every few minutes. The exact rule depends on how many machines you have: Multiple machines. The proxy uses yoursoft_limit concurrency setting to compute excess capacity:
excess ≥ 1, the proxy stops one machine. The + 1 keeps a buffer of one idle machine for incoming traffic.
Single machine. Simpler: if load is zero, the proxy stops the machine.
In both cases, “load” means traffic the proxy can see. Background work running inside the machine, whether that’s async workers, cron-style loops, or anything else not driven by an inbound request, doesn’t count. There’s also no way for your application to tell the proxy, “I’m busy, leave me alone.”
This is the central fact for the rest of the guide. Everything below is a way to work around it.
Stop vs. suspend
auto_stop_machines takes three values: "off", "stop", and "suspend".
- stop shuts the machine down cold. A restart takes seconds (about 2s for a Rails app, less for a small binary).
- suspend dumps the entire VM state (memory, CPU, network) to disk. Resume takes a few hundred milliseconds.
- Machines must have 4 GB of RAM or less.
- Swap and schedules are not supported.
- Machines updated before June 20, 2024 cannot be suspended.
- Suspend is not durable. Fly does not guarantee that a suspended machine will resume. Host migration, maintenance, or capacity pressure can turn what would have been a resume into a cold start. Treat suspend as a faster version of stop, not a guaranteed warm restart.
- A few log lines may be lost across a suspend/resume cycle, and the system clock can take a second or two to re-synchronize after resume. See “Suspend vs. Stop” for details on clock skew.
Pattern A: disable autostop, manage shutdown in the app
Use this when your app has long-lived workers, in-process job runners, or any background work that the application itself can track. Turn autostop off infly.toml:
fly machine stop, or a host migration). You’re paying for every machine 24/7, in every region you’ve scaled into, so make sure that’s the right tradeoff before adopting this pattern.
When deploys, manual stops, or host migrations do stop the machine, your app gets SIGTERM and has kill_timeout seconds to clean up. The default of 5 seconds is almost certainly too short. Bump it. These are top-level keys in fly.toml:
kill_timeout is a drain window, not a “let the job finish” knob. If your jobs run longer than 5 minutes, either checkpoint them so they can resume, or stop accepting new work and let in-flight jobs drain before the timeout. Don’t wait for everything to finish.
A minimal shutdown pattern in Node:
SIGTERM arrives, then wait for in-flight jobs up to a deadline a few seconds shorter than kill_timeout. The safety margin matters, if you wait the full 30s, Fly’s SIGKILL arrives before your exit(0) runs.
Pattern B: split web and worker into separate process groups
Use this when web traffic is bursty (a good candidate for autostop) but background work is steady or long-running (a bad candidate for autostop). Split withprocesses in fly.toml:
[http_service] attached, so the proxy never touches its machines. Autostop applies only to the web tier.
Scale them independently:
Graceful shutdown: what Fly sends
When something stops your machine, whether that’sauto_stop_machines, fly machine stop, a deploy, or a host migration, Fly sends kill_signal (default: SIGTERM) to PID 1. After waiting kill_timeout seconds, it sends SIGKILL.
The defaults are conservative:
Five seconds is enough for an HTTP server to close keepalives. It is not enough for a long-running job to finish. If you have any background work, set
kill_timeout to a value that allows your typical job to complete. You’ll need to determine this on your side. Both keys are top-level in fly.toml:
CMD ["sh", "-c", "..."]), the shell is PID 1 and SIGTERM doesn’t propagate. Use the exec form: CMD ["myapp"], or exec myapp inside the wrapper.
kill_timeout is not a “finish your work” timer. It’s a drain window. Inside it, you should:
- Stop accepting new work
- Let in-flight work finish, or checkpoint it
- Exit cleanly
kill_timeout. You need either Pattern A with checkpoint/resume, or Pattern B with a worker tier that’s never autostopped.
Run fly config validate --strict before relying on any of this. By default, fly config validate silently accepts unrecognized sections and keys. A typo or outdated section name can pass validation and then do nothing at runtime. Strict mode catches those errors.
Picking a pattern
Common problems
MySIGTERM handler runs but the job still gets killed. kill_timeout is shorter than your handler needs. Bump it (max 300s) and set your handler’s deadline a few seconds under that.
The machine stops mid-job even with auto_stop_machines = "off". Autostop is only one of several things that stop machines. Deploys, fly machine stop, scale-down, and host migrations all do too. Check fly logs for the instance refused or host migration events. Pattern A still applies. The only difference is that autostop is no longer the trigger.
Why doesn’t a self-ping keep my machine alive? It won’t. The autostop reference defines idle as “a load of 0” but doesn’t specify what counts as load. Empirically, sending a successful HTTP request every 60 seconds from a machine to its own <app>.fly.dev hostname does not prevent autostop. The proxy still stops the machine after 5 to 10 minutes. To keep a machine running through idle traffic, turn off autostop (Pattern A) or move the work into a process group without [http_service] (Pattern B).
Worker machines won’t stop when I deploy. A process group with no [http_service], such as the worker tier in Pattern B, is invisible to the proxy. Deploys still update those machines because flyctl talks to them directly, but the proxy does not manage their lifecycle and cannot autostop them. To stop them gracefully, send a signal with fly machine stop or let fly deploy replace them during a deployment.
Suspend resumes are slower than the docs say. Suspend isn’t durable. If Fly can’t restore the snapshot (host migration, capacity pressure), you get a cold start. There’s no flag to tell you which happened; check the first-request latency. If cold starts matter, run with min_machines_running = 1.
Where to go next
- Work queues guide: Picking a queue technology
- Task scheduling guide: cron-style triggers and scheduled machines
- Autostart and autostop reference: The proxy’s full decision logic
- Configuration reference:
kill_signal,kill_timeout,processes,auto_stop_machines - Machine states: what
stopping,stopped, andsuspendedactually mean