tokio::select! (Rust): Guide & Docs 2026

Max WellsMax WellsFounder of Rustify

TL;DR: tokio::select! polls multiple async branches concurrently and executes the first one that becomes ready, cancelling the rest. It is the primary tool for racing futures, implementing timeouts, and responding to shutdown signals. Each branch is a pattern + async expression pair. If multiple branches are ready simultaneously, select! picks one at random (to avoid starvation). Be aware of cancellation safety: operations dropped mid-flight may lose data.


What Is tokio::select!?

tokio::select! is a macro that polls multiple async expressions at once and runs whichever branch completes first.

use tokio::time::{sleep, Duration};
 
async fn fetch_with_timeout(url: &str) -> Result<String, &'static str> {
    tokio::select! {
        result = fetch(url) => result.map_err(|_| "fetch failed"),
        _ = sleep(Duration::from_secs(5)) => Err("timed out"),
    }
}

The sleep branch and the fetch branch race. Whichever resolves first wins; the other is dropped.


How Do You Handle Shutdown Signals?

select! is the standard way to listen for a cancellation token or shutdown channel alongside normal work.

use tokio::sync::oneshot;
 
async fn worker(mut shutdown: oneshot::Receiver<()>) {
    loop {
        tokio::select! {
            _ = do_work() => {},
            _ = &mut shutdown => {
                println!("shutting down");
                break;
            }
        }
    }
}

This pattern appears in every production Tokio service: work loop + shutdown receiver in select!.


What Is Cancellation Safety?

A future is cancellation-safe if dropping it mid-poll loses no data. select! cancels losing branches by dropping them.

// tokio::sync::mpsc::Receiver::recv() IS cancellation-safe
// std::io::AsyncReadExt::read_exact() is NOT; partial reads are lost
 
let mut buf = [0u8; 1024];
tokio::select! {
    n = socket.read(&mut buf) => { /* safe */ }
    _ = shutdown.recv() => {}
}

Check the Tokio docs for each async fn's cancellation safety note. For non-safe operations, buffer the result externally before the select!.


How Does select! Differ From join!?

select! runs the first-to-finish branch. join! waits for all branches to finish.

select!join!
Completes whenFirst branch readyAll branches done
Other branchesCancelled (dropped)All run to completion
Use forTimeouts, racing, shutdownParallel independent work
ReturnsOne valueTuple of all values

Use join! when you need all results. Use select! when you need the fastest result or want to cancel on a signal.


Frequently Asked Questions

Yes, but pin it first. Use tokio::pin!(fut) and reference it as &mut fut in each select! to avoid moving it.

select! suspends the task until at least one branch makes progress; it does not spin.

Yes. A else branch runs immediately if all branches are ready to be polled but none would block; rarely needed in practice.


Sources


  • tokio: The async runtime select! runs inside
  • spawn: Alternative for concurrent work that runs to completion
  • channel: Often used with select! for message passing
  • future: What each select! branch polls

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?