Skip to main content
The tokio::signal module provides asynchronous signal handling for both Unix and Windows platforms.
Signal handling is a tricky topic and should be used with great care. Review the platform-specific limitations and caveats before use.

Overview

This module allows you to listen for OS signals in an asynchronous manner, integrating signal handling into Tokio’s async runtime without blocking.

Cross-Platform: Ctrl-C

The ctrl_c function provides a portable way to receive CTRL+C notifications on both Unix and Windows.
ctrl_c
async fn
Completes when a “ctrl-c” notification is sent to the process.Returns: io::Result<()>The future completes on the first CTRL+C received after polling begins.
use tokio::signal;

#[tokio::main]
async fn main() {
    println!("waiting for ctrl-c");
    
    signal::ctrl_c().await.expect("failed to listen for event");
    
    println!("received ctrl-c event");
}

Background Listening

You can spawn a task to listen for CTRL+C in the background:
tokio::spawn(async move {
    tokio::signal::ctrl_c().await.unwrap();
    // Your shutdown handler here
    println!("Shutting down gracefully...");
});

Graceful Shutdown Example

use tokio::signal;
use tokio::sync::broadcast;

#[tokio::main]
async fn main() {
    let (shutdown_tx, _) = broadcast::channel(1);
    
    tokio::spawn(async move {
        signal::ctrl_c().await.expect("failed to listen for ctrl-c");
        println!("Received ctrl-c, shutting down...");
        let _ = shutdown_tx.send(());
    });
    
    // Your application logic here
    // Listen to shutdown_tx.subscribe() to gracefully shut down
}

Unix Signals

Unix signal handling is only available on Unix platforms with the signal feature enabled.
The unix submodule provides Unix-specific signal handling:
use tokio::signal::unix::{signal, SignalKind};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a stream of SIGHUP signals
    let mut stream = signal(SignalKind::hangup())?;
    
    // Print whenever a HUP signal is received
    loop {
        stream.recv().await;
        println!("got signal HUP");
    }
}

SignalKind

Represents specific Unix signals:
SignalKind::alarm
const fn
Represents SIGALRM - real-time timer expired.Default behavior: Process is terminated
let mut alarms = signal(SignalKind::alarm())?;
SignalKind::child
const fn
Represents SIGCHLD - child process status changed.Default behavior: Signal is ignored
let mut child_signals = signal(SignalKind::child())?;
SignalKind::hangup
const fn
Represents SIGHUP - terminal disconnected.Default behavior: Process is terminated
let mut hangups = signal(SignalKind::hangup())?;
SignalKind::interrupt
const fn
Represents SIGINT - interrupt signal (Ctrl-C).Default behavior: Process is terminated
let mut interrupts = signal(SignalKind::interrupt())?;
SignalKind::io
const fn
Represents SIGIO - I/O operations possible on file descriptor.Default behavior: Signal is ignored
let mut io_signals = signal(SignalKind::io())?;
SignalKind::pipe
const fn
Represents SIGPIPE - write to pipe with no reader.Default behavior: Process is terminated
let mut pipe_signals = signal(SignalKind::pipe())?;
SignalKind::quit
const fn
Represents SIGQUIT - quit signal with core dump.Default behavior: Process is terminated with core dump
let mut quits = signal(SignalKind::quit())?;
SignalKind::terminate
const fn
Represents SIGTERM - termination signal.Default behavior: Process is terminated
let mut terms = signal(SignalKind::terminate())?;
SignalKind::user_defined1
const fn
Represents SIGUSR1 - user-defined signal 1.Default behavior: Process is terminated
let mut usr1 = signal(SignalKind::user_defined1())?;
SignalKind::user_defined2
const fn
Represents SIGUSR2 - user-defined signal 2.Default behavior: Process is terminated
let mut usr2 = signal(SignalKind::user_defined2())?;
SignalKind::window_change
const fn
Represents SIGWINCH - terminal window size changed.Default behavior: Signal is ignored
let mut winch = signal(SignalKind::window_change())?;

Custom Signal Numbers

SignalKind::from_raw
const fn
Creates a SignalKind from a raw signal number.Parameters:
  • signum: c_int - Raw signal number
Returns: SignalKindUseful for platform-specific signals:
let kind = SignalKind::from_raw(libc::SIGRTMIN);
let mut stream = signal(kind)?;
SignalKind::as_raw_value
const fn
Gets the signal’s numeric value.Returns: c_int
let kind = SignalKind::interrupt();
assert_eq!(kind.as_raw_value(), libc::SIGINT);

Signal Stream

The signal function returns a stream that yields each time the signal is received:
use tokio::signal::unix::{signal, SignalKind};

let mut stream = signal(SignalKind::terminate())?;

loop {
    stream.recv().await;
    println!("Received SIGTERM");
}

Methods

recv
async fn
Waits for the next signal notification.Returns: Option<()>Returns None if the signal stream is closed.
while let Some(()) = stream.recv().await {
    println!("Signal received");
}

Multiple Signal Handling

You can listen to multiple signals simultaneously using tokio::select!:
use tokio::signal::unix::{signal, SignalKind};

let mut sigterm = signal(SignalKind::terminate())?;
let mut sigint = signal(SignalKind::interrupt())?;
let mut sighup = signal(SignalKind::hangup())?;

loop {
    tokio::select! {
        _ = sigterm.recv() => {
            println!("Received SIGTERM, shutting down gracefully");
            break;
        }
        _ = sigint.recv() => {
            println!("Received SIGINT (Ctrl-C)");
            break;
        }
        _ = sighup.recv() => {
            println!("Received SIGHUP, reloading config");
            // Reload configuration
        }
    }
}

Important Caveats

Signal Handler Replacement

When you register a signal listener, Tokio installs an OS signal handler that replaces the default platform behavior for the entire process duration.
For example, on Unix, processes normally terminate when receiving SIGINT (Ctrl-C). Once you create a signal listener:
let mut sigint = signal(SignalKind::interrupt())?;
The signal is captured by Tokio instead of terminating the process. This persists even after dropping the listener. Your application must explicitly handle the signal and decide whether to exit.

Proper Signal Handling

use tokio::signal::unix::{signal, SignalKind};

let mut sigterm = signal(SignalKind::terminate())?;
let mut sigint = signal(SignalKind::interrupt())?;

tokio::select! {
    _ = sigterm.recv() => {
        println!("SIGTERM received, exiting");
        std::process::exit(0);
    }
    _ = sigint.recv() => {
        println!("SIGINT received, exiting");
        std::process::exit(0);
    }
}

Complete Example

Here’s a complete example showing graceful shutdown with multiple signals:
use tokio::signal::unix::{signal, SignalKind};
use tokio::sync::broadcast;
use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // Create shutdown channel
    let (shutdown_tx, mut shutdown_rx) = broadcast::channel(1);
    
    // Spawn signal handler
    tokio::spawn(async move {
        let mut sigterm = signal(SignalKind::terminate())
            .expect("failed to listen for SIGTERM");
        let mut sigint = signal(SignalKind::interrupt())
            .expect("failed to listen for SIGINT");
        
        tokio::select! {
            _ = sigterm.recv() => println!("SIGTERM received"),
            _ = sigint.recv() => println!("SIGINT received"),
        }
        
        let _ = shutdown_tx.send(());
    });
    
    // Main application loop
    loop {
        tokio::select! {
            _ = shutdown_rx.recv() => {
                println!("Shutting down gracefully...");
                break;
            }
            _ = tokio::time::sleep(tokio::time::Duration::from_secs(1)) => {
                println!("Working...");
            }
        }
    }
    
    println!("Cleanup complete");
    Ok(())
}

Platform-Specific Notes

Unix

  • Signal handlers are process-wide and persist for the process lifetime
  • Real-time signals are supported on Linux and illumos
  • Some signals cannot be caught (e.g., SIGKILL, SIGSTOP)

Windows

Windows has limited signal support. The ctrl_c function is the primary cross-platform option.

See Also

Build docs developers (and LLMs) love