select! & Concurrency
tokio::select! multiplexes multiple async branches, completing when the first one finishes. It is the core pattern for event loops, shutdown handling, and racing I/O with timers.
Search across all documentation pages
tokio::select! multiplexes multiple async branches, completing when the first one finishes. It is the core pattern for event loops, shutdown handling, and racing I/O with timers.
Quick-reference recipe card - copy-paste ready.
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(4);
tokio::select! {
Some(msg) = rx.recv() => println!("{msg}"),
_ = sleep(Duration::from_millis(100)) => println!("timeout"),
else => println!("channel closed"),
}
let _ = tx.send("hi").await;
}When to reach for this: Server main loops, combining channel messages with shutdown signals, and timeout races.
use tokio::sync::mpsc;
use tokio::time::{interval, Duration};
async fn worker(mut rx: mpsc::Receiver<u32>, mut shutdown: mpsc::Receiver<()>) {
let mut ticker = interval(Duration::from_millis(200));
loop {
tokio::select! {
_ = shutdown.recv() => {
println!("shutdown");
break;
}
Some(job) = rx.recv() => {
println!("job {job}");
}
_ = ticker.tick() => {
println!("heartbeat");
}
}
}
}
#[tokio::main]
async fn main() {
let (job_tx, job_rx) = mpsc::channel(8);
let (stop_tx, stop_rx) = mpsc::channel(1);
let handle = tokio::spawn(worker(job_rx, stop_rx));
job_tx.send(1).await.unwrap();
stop_tx.send(()).await.unwrap();
handle.await.unwrap();
}What this demonstrates:
interval tick branch for periodic work.biased; prefix checks branches in declaration order (faster, can starve).| Operation | Cancel-safe in loop? |
|---|---|
mpsc::recv | yes |
sleep | yes |
read partial buffer | often no - use buffer carefully |
async fn with state | depends - audit docs |
// Random fair selection (pattern for rare cases)
use rand::Rng;
let branches = [async { 1 }, async { 2 }];
// Prefer declarative select! for readability in most serversselect!.Semaphore to limit concurrent selected work.select! on multiple JoinHandles races task completion.biased for fairness.sleep or await blocking recv.Send. Fix: Acquire lock inside branch body only.else or break on None.| Alternative | Use When | Don't Use When |
|---|---|---|
join! | Need all branches | Event loop first-ready |
| Dedicated reader task + mpsc | Complex protocol parsing | Simple timer + one channel |
FuturesUnordered | Many dynamic futures | Fixed small branch set |
| epoll manually | Custom runtime | Tokio application code |
select! first completion; join! waits for all. Different use cases.
Checks branches in order without randomization - predictable but can starve later branches.
loop { select! { ... } } is standard for Tokio services until shutdown branch breaks.
One branch per channel recv - classic multiplexed server design.
some_stream.next() => branch supported in tokio::select!.
Some branch futures need pin in select - follow macro expansion errors with pin!.
with_graceful_shutdown uses signal future - related but at server level.
tokio::time::pause + advance for deterministic timer branches.
Put high-priority branch first with biased - document starvation risk.
Use tokio::select! for Tokio futures; futures::select for non-Tokio contexts.
Stack 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 16, 2026