Zero-downtime deployment of Rust services: reload without downtime

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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

Blogs

Book Recommendations