Skip to content

Expert · Concurrency and async · topic 38 of 50 · 35 min

Async Rust with tokio

Explanation

Async Rust handles many tasks that spend most of their time waiting, such as network requests, with few threads. An async fn returns a Future that does nothing until it is awaited, and a runtime such as tokio runs the futures. Use #[tokio::main], .await, tokio::spawn and tokio::join!, and never block the runtime with slow synchronous work.

Why async?

A web server for a food delivery app may hold 10,000 open connections. Most of the time, each one is waiting: for the database, for a payment gateway, for the next request. A thread per connection would need 10,000 threads, each with its own memory. Async programming lets a handful of threads juggle all these waiting tasks, switching to whichever one is ready.

Think of it like this: Threads are like hiring one cook per order: each cook stands idle while the rice boils. Async is like one skilled cook watching ten pots: she starts the rice, chops onions while it boils, stirs the dal, and comes back to each pot exactly when it needs her.

Futures and await

An async fn does not run when you call it. It returns a Future: a value that represents work that will finish later. Futures in Rust are lazy; nothing happens until something awaits them. Writing .await inside another async function says "pause here until this future is done, and let other tasks run meanwhile".

rust
async fn fetch_price(item: &str) -> u32 {
    // imagine a network call here
    if item == "chai" { 15 } else { 40 }
}

async fn total() -> u32 {
    let a = fetch_price("chai").await;
    let b = fetch_price("samosa").await;
    a + b
}
Calling fetch_price(...) alone does nothing. .await drives it to completion.

A runtime runs the futures

The standard library defines what a Future is, but it does not include a runtime to run them. The most popular runtime is tokio. A runtime keeps a set of tasks, polls the ones that are ready to make progress, and uses the operating system to learn when sockets and timers are ready.

ini
[dependencies]
tokio = { version = "1", features = ["full"] }
Cargo.toml. "full" turns on all tokio features; real projects often pick fewer.
rust
use tokio::time::{sleep, Duration};

async fn fetch(name: &str, ms: u64) -> String {
    sleep(Duration::from_millis(ms)).await;      // a pretend network wait
    format!("{name} done")
}

#[tokio::main]
async fn main() {
    // one after another: about 300 ms
    let a = fetch("orders", 100).await;
    let b = fetch("payments", 200).await;
    println!("{a}, {b}");

    // at the same time: about 200 ms
    let (c, d) = tokio::join!(fetch("orders", 100), fetch("payments", 200));
    println!("{c}, {d}");
}
#[tokio::main] starts the runtime and runs async main. join! waits for several futures at once.

Spawning tasks

rust
let handle = tokio::spawn(async {
    fetch("report", 50).await
});
let report = handle.await.unwrap();   // JoinHandle is a future too
tokio::spawn runs a task in the background, like thread::spawn but much lighter: you can have hundreds of thousands.

Spawned tasks may run on any of the runtime's worker threads, so what they capture must be Send and 'static, just like with threads. Use move and Arc in the same way.

Never block the runtime

Async works only if tasks give control back at each .await. If a task does slow work without awaiting, such as std::thread::sleep, a long CPU calculation, or a blocking file or database call, it holds a worker thread hostage and every other task on it stops. Use tokio's async versions (tokio::time::sleep, tokio::fs) and move heavy CPU work to tokio::task::spawn_blocking.

Threads or async?

SituationBetter choice
Thousands of network connections, mostly waitingAsync (tokio)
Heavy CPU work: image processing, number crunchingThreads (or rayon)
A small CLI tool that makes one requestEither; plain blocking code is simplest
A web API server (axum, module 11)Async

Common mistakes

  • Calling an async fn and forgetting .await: nothing happens, and the compiler warns "unused implementer of Future that must be used".
  • Using std::thread::sleep inside async code. Use tokio::time::sleep(...).await.
  • Awaiting in a loop one by one when the calls are independent. Use join! or spawn several tasks.
  • Holding a std Mutex guard across an .await. Keep the guard in a small block, or use tokio::sync::Mutex when you must hold it across awaits.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Async Rust with tokio · 1/7

Many waiting tasks, few threads

Async is for many tasks that spend most of their time waiting, like network calls.

Prefer a video by a teacher? Find free videos on “rust async await tokio tutorial futures explained” on YouTube ↗

In real life: Checking train status for many routes

A travel app shows live status for 20 trains on one screen. Each status comes from a slow external API (about 300 ms). Fetching one after another would take 6 seconds. The backend spawns one tokio task per train and awaits them all, so the whole screen loads in about the time of the slowest single call, using just a few threads.

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

async fn status(train: u32) -> String {
    sleep(Duration::from_millis(300)).await;   // pretend API call
    format!("{train}: on time")
}

#[tokio::main]
async fn main() {
    let trains = [12951, 12002, 22691];
    let mut handles = Vec::new();
    for t in trains {
        handles.push(tokio::spawn(status(t)));
    }
    for h in handles {
        println!("{}", h.await.unwrap());
    }
}
All three requests wait at the same time: about 300 ms in total, not 900 ms.

Practice (0/3 done)

Task 1 · Question

What happens here inside an async main? async fn hello() { println!("hi"); } hello();

Task 2 · Question

Inside an async task you need to wait 2 seconds. Which is correct?

Task 3 · Practice

Create a cargo project with tokio. Write async fn fake_fetch(id: u32) -> u32 that sleeps 200 ms and returns id * 10. Fetch ids 1 to 5 one by one and time it, then fetch them concurrently with tokio::spawn (or join!) and time it again. Print both durations.

Quick quiz

Question 1

1.What does an async fn return when called?

Question 2

2.Why does Rust need a runtime like tokio for async?

Question 3

3.What is the danger of a long CPU loop inside an async task?

Question 4

4.Which is the best fit for a server with 50,000 mostly idle connections?

Question 5

5.What does tokio::join!(a, b) do?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does an async fn return when called?

Answer: A Future

The body runs only when the future is awaited or spawned.

Why does Rust need a runtime like tokio for async?

Answer: The standard library has no executor to run futures

std defines Future but does not ship an executor.

What is the danger of a long CPU loop inside an async task?

Answer: It blocks the worker thread, so other tasks stall

Tasks must yield at .await points; use spawn_blocking for heavy work.

Which is the best fit for a server with 50,000 mostly idle connections?

Answer: Async tasks on a runtime

Async tasks are cheap and ideal for waiting on I/O.

What does tokio::join!(a, b) do?

Answer: Runs a and b concurrently and waits for both

join! polls both futures at the same time.

Where does an error from an awaited Future appear?Related

Answer: At the await, where try/catch can handle it

It is rethrown at the await.

From Futures and async/await

What kind of work benefits most from async/await?Related

Answer: Waiting for network, disk or database (I/O)

async frees threads during I/O waits.

From async and await: waiting without blocking

Why pass std::launch::async to std::async?Related

Answer: To require running on a new thread instead of possibly deferring until get()

Without it, the implementation may defer the call.

From Concurrency 2: atomics, async, futures and condition variables

async function f() { return 3; } What is f()?Related

Answer: A promise that fulfils with 3

async functions always return promises.

From async and await

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Concurrency and async”. The module exam comes next.
  2. Module 9 exam: Concurrency and async · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Testing, tooling and unsafe Rust. 50 topics left in the course before the final exam.

← Previous: Shared state: Arc, Mutex, Send and Sync