Skip to content

Graceful shutdown with Tokio and Axum

A production process should not treat termination as “the operating system will kill it eventually.” The useful pattern is: detect shutdown, notify the server, stop accepting new work, let in-flight work finish where possible, and explicitly stop application-owned background tasks.

Tokio describes graceful shutdown as three separate problems: deciding when to shut down, telling parts of the program to stop, and waiting for them to finish.

Handle the signals your process actually receives

Section titled “Handle the signals your process actually receives”

For local development, Ctrl+C is the obvious signal. On Unix deployments and in containers, SIGTERM is also a normal termination path.

EXAMPLE: CI-CHECKEDCOMPILED · examples/axum-backend/src/main.rs#recipe-graceful-shutdown
async fn shutdown_signal() {
    let ctrl_c = async {
        if let Err(error) = signal::ctrl_c().await {
            tracing::error!(%error, "failed to install Ctrl+C handler");
            std::future::pending::<()>().await;
        }
    };

    #[cfg(unix)]
    let terminate = async {
        match signal::unix::signal(signal::unix::SignalKind::terminate()) {
            Ok(mut stream) => {
                stream.recv().await;
            }
            Err(error) => {
                tracing::error!(%error, "failed to install SIGTERM handler");
                std::future::pending::<()>().await;
            }
        }
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {}
        _ = terminate => {}
    }

    tracing::info!("shutdown signal received");
}

The Unix branch is conditionally compiled. On non-Unix targets the second future never completes, so Ctrl+C remains the portable shutdown path.

If installing a signal handler fails, the example logs the failure and keeps the failed branch pending instead of pretending that a shutdown signal was received.

Axum’s Serve::with_graceful_shutdown takes a future. When that future completes, the server begins its graceful shutdown path.

The Axum REST API blueprint wires the checked signal future directly into the server startup.

Keep this distinction clear:

  • the signal future decides when shutdown starts
  • Axum handles the HTTP server shutdown
  • your application still owns any tasks it spawned independently

Background tasks need their own cancellation path

Section titled “Background tasks need their own cancellation path”

A task started with tokio::spawn does not become magically owned by Axum because the HTTP server is shutting down.

Tokio’s shutdown guidance recommends cancellation tokens for notifying multiple tasks and a task tracker when the process must wait for them to finish. Those utilities live in tokio-util.

A service with workers might therefore use this order:

  1. receive Ctrl+C or SIGTERM
  2. stop accepting new HTTP work
  3. cancel background workers
  4. close producers that can create more work
  5. wait for tracked tasks with an explicit deadline policy
  6. let the process exit

The exact order depends on your dependencies. A queue consumer, a database writer, and an HTTP-only service do not have the same shutdown semantics.

Graceful shutdown needs a failure policy. If one request or worker can block forever, “wait for everything” can turn into “never terminate.”

Decide which operations have deadlines before shutdown happens. For orchestrated deployments, also make sure the application’s maximum shutdown time fits inside the platform’s termination grace period.

Tokio notes that signal handling has platform-specific caveats. Registering handlers changes process-level behavior, so keep signal installation in your binary startup code rather than hiding it inside request handlers or reusable library functions.

For a small service, one shutdown future is enough. When the application gains independent workers, promote cancellation and task tracking to explicit application infrastructure.