RustAsync Runtime (tokio)

Async Runtime: tokio

tokio is Rust's most widely used asynchronous runtime. It provides the executor that drives async/await futures to completion, plus a rich ecosystem of async-native utilities: I/O, timers, task scheduling, synchronisation primitives, and networking.

Rust's async model is poll-based and lazy — a future does nothing until something polls it. tokio is the engine that does the polling, running hundreds of thousands of lightweight tasks concurrently on a small thread pool.

Adding tokio to Cargo.toml

TOML
[dependencies]
tokio = { version = "1", features = ["full"] }
Note
The full feature enables everything: I/O, timers, networking, sync primitives, and macros. In production you can trim this to only the features you use (e.g. features = ["rt-multi-thread", "macros", "time"]) to reduce compile times.
#[tokio::main]

The #[tokio::main] attribute macro transforms an async fn main() into a synchronous entry point by starting the tokio runtime and blocking until the future completes. It is the standard way to enter async Rust.

RUST
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    println!("start");
    sleep(Duration::from_millis(500)).await;
    println!("done after 500ms");
}
start
done after 500ms
Tip
#[tokio::main] expands to roughly: fn main() { tokio::runtime::Runtime::new().unwrap().block_on(async { ... }) }. You can also build the runtime manually for more control over its configuration.
tokio::spawn — Concurrent Tasks

tokio::spawn spawns a new async task that runs concurrently with the current task. It returns a JoinHandle<T> that you can .await to get the result, similar to std::thread::spawn but for async tasks.

RUST
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let handle1 = tokio::spawn(async {
        sleep(Duration::from_millis(200)).await;
        println!("task 1 done");
        42
    });

    let handle2 = tokio::spawn(async {
        sleep(Duration::from_millis(100)).await;
        println!("task 2 done");
        99
    });

    // Both tasks run concurrently
    let result1 = handle1.await.unwrap();
    let result2 = handle2.await.unwrap();
    println!("results: {} and {}", result1, result2);
}
task 2 done
task 1 done
results: 42 and 99
Note
Spawned tasks must be 'static and Send — they may be moved to a different thread in the pool. Use Arc to share data across spawned tasks.
tokio::join! — Wait for All

tokio::join! runs multiple futures concurrently within the same task and waits until all of them finish. Unlike spawning, join does not create new tasks — it interleaves the futures on the current task, which is more efficient for small amounts of work.

RUST
use tokio::time::{sleep, Duration};

async fn fetch_users() -> Vec<String> {
    sleep(Duration::from_millis(100)).await;
    vec![String::from("alice"), String::from("bob")]
}

async fn fetch_config() -> String {
    sleep(Duration::from_millis(80)).await;
    String::from("config_v2")
}

#[tokio::main]
async fn main() {
    // Both futures run concurrently — total time ≈ 100ms, not 180ms
    let (users, config) = tokio::join!(fetch_users(), fetch_config());
    println!("users: {:?}", users);
    println!("config: {}", config);
}
users: ["alice", "bob"]
config: config_v2
tokio::select! — First One Wins

tokio::select! runs multiple futures concurrently and proceeds with whichever completes first, cancelling the rest. This is useful for timeouts, racing alternative data sources, or responding to a shutdown signal.

RUST
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let slow = async {
        sleep(Duration::from_secs(5)).await;
        "slow result"
    };

    let fast = async {
        sleep(Duration::from_millis(100)).await;
        "fast result"
    };

    let winner = tokio::select! {
        r = slow => r,
        r = fast => r,
    };

    println!("winner: {}", winner);
}
winner: fast result
Timer Utilities

The tokio::time module provides async-aware timer primitives that yield control back to the runtime instead of blocking the thread.

RUST
use tokio::time::{sleep, interval, timeout, Duration};

#[tokio::main]
async fn main() {
    // One-shot delay
    sleep(Duration::from_millis(50)).await;
    println!("slept 50ms");

    // Repeating interval — tick every 200ms, print 3 times
    let mut ticker = interval(Duration::from_millis(200));
    for i in 0..3 {
        ticker.tick().await;  // first tick fires immediately
        println!("tick {}", i);
    }

    // Timeout — cancel a future if it takes too long
    let slow_op = async {
        sleep(Duration::from_secs(10)).await;
        "done"
    };

    match timeout(Duration::from_millis(100), slow_op).await {
        Ok(val) => println!("got: {}", val),
        Err(_)  => println!("operation timed out"),
    }
}
slept 50ms
tick 0
tick 1
tick 2
operation timed out
Async File I/O

tokio::fs mirrors std::fs but with async versions of every function. File operations yield to the runtime while waiting, so other tasks can continue.

RUST
use tokio::fs;
use tokio::io::AsyncWriteExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Write a file
    let mut file = fs::File::create("hello.txt").await?;
    file.write_all(b"Hello, tokio!
").await?;

    // Read the whole file as a String
    let contents = fs::read_to_string("hello.txt").await?;
    println!("contents: {}", contents.trim());

    // Clean up
    fs::remove_file("hello.txt").await?;
    Ok(())
}
contents: Hello, tokio!
Async TCP Networking

tokio::net provides async TCP and UDP sockets. The pattern for a TCP server is: bind a TcpListener, accept connections in a loop, and spawn a task per connection.

RUST
use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

async fn handle_connection(mut stream: TcpStream) {
    let mut buf = [0u8; 1024];
    let n = stream.read(&mut buf).await.unwrap_or(0);
    if n > 0 {
        // Echo the data back
        stream.write_all(&buf[..n]).await.ok();
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("listening on :8080");

    loop {
        let (stream, addr) = listener.accept().await?;
        println!("connection from {}", addr);
        tokio::spawn(handle_connection(stream));
    }
}
Note
Each connection is handled in its own spawned task, so thousands of connections run concurrently with very little memory — tokio tasks are much lighter than OS threads.
Synchronisation Primitives

tokio provides async-aware versions of the standard synchronisation types. Using std::sync::Mutex inside async code works but will block the runtime thread while waiting — always use tokio::sync equivalents instead.

Type

Use case

tokio::sync::Mutex

Async-aware mutex — will not block the runtime thread while waiting

tokio::sync::RwLock

Async read-write lock — many readers or one writer

tokio::sync::oneshot

Send exactly one value between tasks

tokio::sync::mpsc

Async multi-producer, single-consumer channel

tokio::sync::broadcast

One sender, many receivers (fan-out)

tokio::sync::Notify

Wake a waiting task without sending a value

RUST
use std::sync::Arc;
use tokio::sync::{Mutex, mpsc};

#[tokio::main]
async fn main() {
    // --- Shared Mutex across tasks ---
    let counter = Arc::new(Mutex::new(0u32));

    let mut handles = vec![];
    for _ in 0..5 {
        let c = Arc::clone(&counter);
        handles.push(tokio::spawn(async move {
            let mut lock = c.lock().await;
            *lock += 1;
        }));
    }
    for h in handles { h.await.unwrap(); }
    println!("counter: {}", *counter.lock().await);

    // --- mpsc channel ---
    let (tx, mut rx) = mpsc::channel::<String>(32);

    tokio::spawn(async move {
        tx.send(String::from("hello")).await.unwrap();
        tx.send(String::from("world")).await.unwrap();
        // tx dropped here — channel closes
    });

    while let Some(msg) = rx.recv().await {
        println!("received: {}", msg);
    }
}
counter: 5
received: hello
received: world
oneshot and broadcast

RUST
use tokio::sync::{oneshot, broadcast};

#[tokio::main]
async fn main() {
    // oneshot — send one value from one task to another
    let (tx, rx) = oneshot::channel::<u32>();
    tokio::spawn(async move {
        tx.send(42).ok();
    });
    println!("oneshot received: {}", rx.await.unwrap());

    // broadcast — multiple receivers get every message
    let (btx, mut brx1) = broadcast::channel::<String>(16);
    let mut brx2 = btx.subscribe();

    btx.send(String::from("broadcast message")).unwrap();

    println!("rx1: {}", brx1.recv().await.unwrap());
    println!("rx2: {}", brx2.recv().await.unwrap());
}
oneshot received: 42
rx1: broadcast message
rx2: broadcast message
spawn_blocking — Running CPU-Bound Work

The tokio runtime uses a small thread pool for async tasks. Long-running synchronous (CPU-bound) work would block a thread and starve other tasks. Use tokio::task::spawn_blocking to run blocking work on a dedicated thread pool that does not interfere with the async executor.

RUST
#[tokio::main]
async fn main() {
    println!("before blocking");

    // Runs on a separate blocking thread pool
    let result = tokio::task::spawn_blocking(|| {
        // Simulate CPU-heavy work
        let mut sum = 0u64;
        for i in 0..1_000_000u64 {
            sum += i;
        }
        sum
    }).await.unwrap();

    println!("sum: {}", result);
    println!("after blocking");
}
before blocking
sum: 499999500000
after blocking
Tip
Good candidates for spawn_blocking: hashing, compression, synchronous database drivers, blocking file reads, and any third-party library that does not support async.
Runtime Flavors

tokio supports two runtime configurations, selectable via the #[tokio::main] macro or when constructing the runtime manually.

Flavor

Threads

Best for

multi_thread (default)

One per CPU core

Production servers, high concurrency workloads

current_thread

1 (caller thread)

CLIs, tests, wasm, single-threaded environments

RUST
// Multi-threaded (default) — spawns one thread per CPU core
#[tokio::main]
async fn main() { /* ... */ }

// Single-threaded — useful for tests and CLIs
#[tokio::main(flavor = "current_thread")]
async fn main() { /* ... */ }

// Manual runtime with custom config
fn main() {
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(4)
        .enable_all()
        .build()
        .unwrap();

    rt.block_on(async {
        println!("running on custom runtime");
    });
}
Common Mistake: Blocking the Runtime
Warning
Never use std::thread::sleep or other blocking calls inside an async function. They block the runtime thread and prevent other tasks from progressing — the entire thread pool can grind to a halt.

RUST
// WRONG — blocks the runtime thread
async fn bad_delay() {
    std::thread::sleep(std::time::Duration::from_secs(1)); // blocks!
}

// CORRECT — yields control back to the runtime
async fn good_delay() {
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}

// CORRECT for blocking stdlib code — run it off the async thread pool
async fn read_blocking_file(path: &str) -> String {
    let path = path.to_string();
    tokio::task::spawn_blocking(move || {
        std::fs::read_to_string(path).unwrap()
    }).await.unwrap()
}
Putting It Together — a Task Pipeline

RUST
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel::<u32>(100);

    // Producer: sends 5 items then closes
    let producer = tokio::spawn(async move {
        for i in 0..5u32 {
            sleep(Duration::from_millis(50)).await;
            tx.send(i * i).await.unwrap();
            println!("sent: {}", i * i);
        }
        // tx dropped — channel closes
    });

    // Consumer: processes until channel is closed
    let consumer = tokio::spawn(async move {
        while let Some(val) = rx.recv().await {
            println!("processed: {}", val);
        }
        println!("channel closed");
    });

    tokio::join!(producer, consumer).0.unwrap();
}
sent: 0
processed: 0
sent: 1
processed: 1
sent: 4
processed: 4
sent: 9
processed: 9
sent: 16
processed: 16
channel closed
Success
tokio turns Rust into a high-performance async platform. With lightweight tasks, async I/O, composable channels, and a battle-tested runtime, it underpins some of the fastest web servers, databases, and messaging systems written in any language.