Skip to content

Tokio

Networking & ConcurrencyConcurrency/ParallelismRust

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 full

Getting started

The smallest useful thing you can do with it, and what each part means.

Tasks and concurrency
#[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(())
}
join! runs futures concurrently on one task; spawn moves work onto the scheduler. The double ?? on a JoinHandle is easy to miss — one for the join, one for the task result.
Channels between tasks
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 full
A bounded channel gives you backpressure for free: a fast producer blocks rather than growing the queue until memory runs out.

Advanced usage

Where the library earns its place over a simpler alternative.

Blocking work and graceful shutdown
// 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();
spawn_blocking is essential: file I/O through std, password hashing or any CPU-bound loop will stall every other task on that thread if run directly.

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.