What it is
Tokio is the dominant asynchronous runtime for Rust, providing a work-stealing scheduler, async networking, timers, synchronisation primitives and task management.
Annotate main with #[tokio::main] and spawn tasks with tokio::spawn. Tokio supplies async versions of TCP, files, channels, mutexes and timers.
Installation
cargo add tokio --features fullGetting started
The smallest useful thing you can do with it, and what each part means.
rust
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Run concurrently on the same task.
let (a, b) = tokio::join!(fetch("/a"), fetch("/b"));
// Spawn onto the scheduler — runs on any worker thread.
let handle = tokio::spawn(async move { expensive_io().await });
let result = handle.await??; // JoinError, then the task's own error
// Race: whichever finishes first wins, the other is dropped.
tokio::select! {
res = fetch("/fast") => println!("{res:?}"),
_ = tokio::time::sleep(Duration::from_secs(1)) => println!("timed out"),
}
Ok(())
}rust
let (tx, mut rx) = tokio::sync::mpsc::channel::<Job>(100);
tokio::spawn(async move {
while let Some(job) = rx.recv().await {
process(job).await;
}
// Loop ends when every sender is dropped.
});
tx.send(job).await?; // applies backpressure when the buffer is fullAdvanced usage
Where the library earns its place over a simpler alternative.
rust
// A synchronous call on a worker thread starves the whole runtime.
let hash = tokio::task::spawn_blocking(move || {
bcrypt::hash(&password, 12) // CPU-bound, blocking
}).await??;
// Cancellation that propagates to every task.
let token = CancellationToken::new();
let child = token.clone();
tokio::spawn(async move {
tokio::select! {
_ = child.cancelled() => cleanup().await,
_ = work() => {}
}
});
tokio::signal::ctrl_c().await?;
token.cancel();Errors and fixes
The failures you are most likely to hit, and what actually resolves them.
- there is no reactor running
- An async operation ran outside a runtime. Use #[tokio::main] or construct a Runtime explicitly.
- The application hangs under load
- Something is blocking a worker thread. Look for synchronous I/O or a std Mutex held across an await.
Best practices
- Never block inside an async function — use spawn_blocking for synchronous or CPU-bound work.
- Prefer bounded channels so a slow consumer applies backpressure.
- Hold std::sync::Mutex only for non-async critical sections; use tokio::sync::Mutex when a lock must be held across an await.
- Enable only the features you use rather than `full` in libraries.
Background
Why it exists, and what it was reacting to.
Rust's async/await is a language feature with no built-in runtime. Tokio filled that gap and became the de facto standard, so most of the async ecosystem assumes it.
