Signals & Process Control
Handle OS signals and manage child processes in long-running Rust services and CLIs.
Search across all documentation pages
Handle OS signals and manage child processes in long-running Rust services and CLIs.
use tokio::signal;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tokio::select! {
_ = signal::ctrl_c() => {
eprintln!("shutting down gracefully");
}
res = run_server() => res?,
}
Ok(())
}When to reach for this: Servers, workers, and CLIs that must shut down cleanly on Ctrl+C or SIGTERM.
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::signal;
use tokio::time::{sleep, Duration};
struct Supervisor {
shutdown: Arc<AtomicBool>,
child: Option<Child>,
}
impl Supervisor {
fn spawn_worker(&mut self) -> anyhow::Result<()> {
let child = Command::new("worker-bin")
.stdout(Stdio::piped())
.spawn()?;
self.child = Some(child);
Ok(())
}
fn stop_worker(&mut self) {
if let Some(mut child) = self.child.take() {
let _ = child.kill();
let _ = child.wait();
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let shutdown = Arc::new(AtomicBool::new(false));
let mut sup = Supervisor { shutdown: shutdown.clone(), child: None };
sup.spawn_worker()?;
tokio::select! {
_ = signal::ctrl_c() => {
shutdown.store(true, Ordering::SeqCst);
sup.stop_worker();
}
_ = async {
while !shutdown.load(Ordering::SeqCst) {
sleep(Duration::from_secs(1)).await;
}
} => {}
}
Ok(())
}What this demonstrates:
ctrl_c() integrates with Tokio's async runtimeCommand::spawn launches child processeskill + wait prevents zombie processes| Signal | Typical use |
|---|---|
| SIGINT | Ctrl+C from terminal |
| SIGTERM | Container orchestrator stop |
| SIGHUP | Reload config (daemons) |
On Unix, use tokio::signal::unix::signal for finer control.
use std::os::unix::process::CommandExt;
unsafe {
cmd.pre_exec(|| {
libc::setpgid(0, 0);
Ok(())
});
}Put children in their own process group to signal them as a unit.
wait leaks PIDs. Fix: Always reap children after they exit.kill may corrupt child state. Fix: Send SIGTERM first; escalate to SIGKILL after timeout.tokio::signal::ctrl_c and ctrl_break on Windows.AtomicBool guard and idempotent shutdown logic.| Alternative | Use When | Don't Use When |
|---|---|---|
signal-hook crate | Sync apps without Tokio | Already on Tokio runtime |
| systemd notify | Service readiness protocol | Simple CLI tools |
| Process supervisor (systemd, k8s) | Production fleet management | Local dev scripts |
Stop accepting work, drain in-flight requests, flush logs, then exit 0.
30 seconds is common for HTTP servers. Document your timeout in ops runbooks.
Re-read config file and swap Arc<Config> atomically. Do not restart listeners blindly.
Read child.stdout in a dedicated thread or tokio::process async task.
Map child exit code to parent: std::process::exit(status.code().unwrap_or(1)).
Use process groups or prctl on Linux to kill children when parent dies.
Send signals in integration tests with nix::sys::signal::kill on the test process child.
Write PID to /var/run/app.pid for init scripts. Remove on shutdown.
Prefer systemd or container entrypoints over classic double-fork daemons.
Set ulimit via prlimit on Linux before spawning heavy worker children.
Command fundamentalsStack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.
Reviewed by Chris St. John·Last updated Jul 19, 2026