Expert · Concurrency and async · topic 38 of 50 · 35 min
Async Rust with tokio
Explanation
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.
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".
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
}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.
[dependencies]
tokio = { version = "1", features = ["full"] }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}");
}Spawning tasks
let handle = tokio::spawn(async {
fetch("report", 50).await
});
let report = handle.await.unwrap(); // JoinHandle is a future tooSpawned 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?
| Situation | Better choice |
|---|---|
| Thousands of network connections, mostly waiting | Async (tokio) |
| Heavy CPU work: image processing, number crunching | Threads (or rayon) |
| A small CLI tool that makes one request | Either; 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
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.
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());
}
}Practice (0/3 done)
What happens here inside an async main? async fn hello() { println!("hi"); } hello();
Inside an async task you need to wait 2 seconds. Which is correct?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Message passing with channelsConcurrency and async · same module
- Futures and async/awaitFlutter and Dart · Async Dart and packages
- async and await: waiting without blockingC# and .NET · Exceptions, delegates, events and async
- Concurrency 2: atomics, async, futures and condition variablesC++ Programming (modern C++ and DSA) · Exceptions, concurrency and building projects with CMake
- async and awaitJavaScript Programming (language in depth) · Asynchronous JavaScript
- Rust for the web with axumReal projects: CLI, web and WebAssembly
Questions people ask about this
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.
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.
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
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
- This is the last topic of “Concurrency and async”. The module exam comes next.
- Module 9 exam: Concurrency and async · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Testing, tooling and unsafe Rust. 50 topics left in the course before the final exam.