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.
Recipe
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.
Working Example
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:
- Long-running loop with multiple event sources.
- Shutdown branch exits cleanly.
intervaltick branch for periodic work.
Deep Dive
How It Works
- First ready branch wins: Other branches are disabled for this iteration.
- Cancellation: Losing branches are dropped at await points.
- biased: Optional
biased;prefix checks branches in declaration order (faster, can starve). - else: Runs when all branches are disabled (e.g., closed channel).
Cancel Safety
| 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 |
Rust Notes
// Random fair selection (pattern for rare cases)
use rand::Rng;
let branches = [async { 1 }, async { 2 }];
// Prefer declarative select! for readability in most servers- Document which branches are safe to cancel in loop
select!. - Combine with
Semaphoreto limit concurrent selected work. select!on multipleJoinHandles races task completion.
Gotchas
- Non-cancel-safe read in loop - Partial read lost on cancel. Fix: Buffer per protocol rules or use dedicated reader task.
- biased select starvation - First branch always ready blocks others. Fix: Reorder or remove
biasedfor fairness. - select without loop exit - Busy spin if branch immediately ready. Fix: Add
sleepor await blocking recv. - Mutex across select - Guard not
Send. Fix: Acquire lock inside branch body only. - Forgotten else on closed channels - Spin on disabled branches. Fix: Use
elseor break onNone.
Alternatives
| 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 |
FAQs
select! vs join!?
select! first completion; join! waits for all. Different use cases.
What does biased do?
Checks branches in order without randomization - predictable but can starve later branches.
Loop pattern?
loop { select! { ... } } is standard for Tokio services until shutdown branch breaks.
Multiple channels?
One branch per channel recv - classic multiplexed server design.
select with streams?
some_stream.next() => branch supported in tokio::select!.
pin required?
Some branch futures need pin in select - follow macro expansion errors with pin!.
Axum shutdown?
with_graceful_shutdown uses signal future - related but at server level.
testing select?
tokio::time::pause + advance for deterministic timer branches.
priority events?
Put high-priority branch first with biased - document starvation risk.
select across crates?
Use tokio::select! for Tokio futures; futures::select for non-Tokio contexts.
Related
- Combining Futures - join and select theory
- Channels in Tokio - recv branches
- Timers & Intervals - sleep and interval
- Graceful Shutdown - shutdown select
- Cancellation & Timeouts - cancel safety
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+.