Zero-downtime deployment of Rust services: how to reload an application without losing clients
Imagine a situation: you've rolled out a new version of your Rust service, but at the moment of restart, 50 users received a 502 error. Someone lost data, someone lost money, and someone lost trust in your product. Familiar pain? Today we'll break down how to set up zero-downtime deployment of Rust services so that updates go unnoticed by clients.
Zero-downtime (or deployment without downtime) is not magic, but a set of technical techniques: graceful shutdown, proper connection management, and traffic balancing. Rust with its type system and async runtime (Tokio) gives us the tools to elegantly solve this problem. Let's go!
The problem: why a regular restart kills production
When you simply kill the process (kill -9) or call std::process::exit(), the operating system instantly closes all sockets. Clients who were sending requests at that moment receive Connection reset or Timeout. For HTTP services, this is a disaster: browsers show an error, mobile apps crash, and background tasks lose state.
In Rust, we can fix this. We need to:
- Stop accepting new requests (but finish the old ones).
- Wait for all active handlers to complete.
- Only then terminate the process.
This is called graceful shutdown — the foundation of zero-downtime deployment of Rust services.
Step 1. Graceful shutdown in Rust with Tokio
Tokio provides built-in mechanisms for graceful shutdown. Let's look at an example of an HTTP server using axum (a popular web framework in Rust).
Basic server without graceful shutdown
use axum::{Router, routing::get};use std::net::SocketAddr;
#[tokio::main]async fn main() { let app = Router::new().route("/", get(|| async { "Hello, World!" }));
let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap();}If you press Ctrl+C, the server simply crashes — all active requests are interrupted. Not good.
Adding graceful shutdown
use axum::{Router, routing::get};use std::net::SocketAddr;use tokio::signal;
async fn shutdown_signal() { // Wait for SIGTERM or SIGINT (Ctrl+C) let ctrl_c = async { signal::ctrl_c() .await .expect("failed to install Ctrl+C handler"); };
#[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() .await; };
#[cfg(not(unix))] let terminate = std::future::pending::<()>();
tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, }
println!("Shutdown signal received, starting graceful shutdown...");}
#[tokio::main]async fn main() { let app = Router::new().route("/", get(|| async { "Hello, World!" }));
let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); axum::Server::bind(&addr) .serve(app.into_make_service()) .with_graceful_shutdown(shutdown_signal()) .await .unwrap();
println!("Server has shut down.");}Now, upon receiving SIGTERM or Ctrl+C, the server:
- Stops accepting new connections.
- Waits for all current requests to complete (with a default timeout).
- Terminates the process.
This is the first step towards zero-downtime deployment of Rust services. But if you have a single server, new clients will not be able to connect while waiting for requests to finish. Need