8.5 Before the Door Closes
The ledger now shows everything. Every request leaves a record: what arrived, what was
returned, when and why. One thing cannot be recorded: the moment the ledger stops. Ctrl+C
leaves no entry. The process disappears - and in the ledger, whatever came before it. An end
without a mark.
What You Need
Ctrl+C sends the process a termination signal. By default the process stops immediately -
regardless of what was happening inside. A request in the middle of processing is cut short:
the client receives no response, a write to disk may remain incomplete.
Graceful shutdown addresses this in three steps: stop accepting new connections, wait for in-flight requests to finish, exit. Axum supports this directly - you only need to describe when exactly the server should stop.
Reproducing the Problem
To see the problem, you need a request that lives long enough. Add a handler to
crates/api/src/routes.rs that intentionally waits 30 seconds:
use std::time::Duration;
use tokio::time::sleep;
async fn slow() -> &'static str {
sleep(Duration::from_secs(30)).await;
"done\n"
}
And in the router function - the route:
.route("/slow", get(slow))
Open two terminals. In the first - start the server:
$ make serve
2026-06-25T10:00:00Z INFO tq_api: listening on 0.0.0.0:3000
In the second - send a request:
$ curl localhost:3000/slow
Return to the first terminal and press Ctrl+C. In the second:
curl: (52) Empty reply from server
The server disappears. The connection drops. The request vanishes without a trace.
The Build
In crates/api/src/main.rs - a new signal function:
async fn shutdown_signal() {
tokio::signal::ctrl_c().await.unwrap();
tracing::info!("shutdown signal received");
}
And at the end of main, replacing axum::serve(listener, app).await.unwrap():
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
tracing::info!("server stopped");
tokio::signal::ctrl_c() waits for Ctrl+C and completes exactly once.
.with_graceful_shutdown(shutdown_signal()) tells the server: when shutdown_signal()
completes - stop accepting new connections, wait for current requests to finish, and only
then exit. The tracing::info!("server stopped") line executes when the server has actually
stopped - not before.
The Result
Repeat the scenario from Reproducing the Problem: make serve in one window,
curl localhost:3000/slow in the second, Ctrl+C in the first. Now in the second window -
not a dropped connection, but a response:
$ curl localhost:3000/slow
done
And in the first window the log shows the full path to the end:
2026-06-25T10:00:00Z INFO tq_api: listening on 0.0.0.0:3000
^C
2026-06-25T10:00:05Z INFO tq_api: shutdown signal received
2026-06-25T10:00:35Z INFO tq_api: server stopped
The ledger now has a mark for the end. make ci passes.
The complete
tqcode for this chapter is in8-ready/05-before-the-door-closes/.
Lore: Signals
Ctrl+C is not just a key combination. The terminal converts it to SIGINT - an operating
system signal sent to the process. Without handling it, the process terminates immediately.
tokio::signal::ctrl_c() intercepts this signal - instead of immediate death, the process
waits for it through .await and decides itself what to do next. with_graceful_shutdown
uses that opportunity: finish reading, finish writing, close connections.
Lore: When It Matters
/slow is a synthetic example: real requests take milliseconds, and catching one at the
moment of shutdown alone is difficult. Under high load this stops being rare - at a thousand
requests per second several are always in flight. Without graceful shutdown, every restart
drops some of them.