join! vs awaiting one by one

Read the explanation
What if

Each .await finishes before the next line runs, so fetch("b") starts only after fetch("a") is done.

Predict first

Each fetch waits 100 ms on the network. When does fetch("b") start?

012345678910111213fetch("a").awaitfetch("b").awaitprint a ba startssleep(100 ms).awaita endsb startssleep(100 ms).awaitb ends

Tap any block to see what that thread is doing.

  • Waiting / parked
  • Running
  • Waiting on I/O
Step 1/4

main awaits fetch("a"). The next line doesn't run until it's done.

1async fn fetch(name: &str) -> &str {2    println!("{name} starts");3    sleep(Duration::from_millis(100)).await; // a network call4    println!("{name} ends");5    name6}7 8#[tokio::main]9async fn main() {10    let a = fetch("a").await;11    let b = fetch("b").await; // starts only after a is done12    println!("{a} {b}");13}

Explanation

What just happened

fetch("a").await finished before the next line ran. So fetch("b") started only after a was done, even though the two calls had nothing to do with each other. Two 100 ms calls took about 200 ms.

While a waited on its call, the thread was free. There just wasn’t anything else started to run on it.

Try the what-if switch

Why

A future does nothing until something polls it. .await polls one future until it’s done. join! polls several, switching whenever one is waiting.

Watch out

join! is concurrency, not parallelism. Everything runs on the caller’s thread, so CPU-heavy futures still take turns. Use tokio::spawn for those.

Common questions

Does .await run futures concurrently?

No. .await waits for that future to finish before the next line runs, so two awaits in a row run one after the other.

Does tokio::join! run futures in parallel?

It runs them concurrently on the calling task: while one waits, it polls the other. They all run on the caller's thread, never on other worker threads.

join! or tokio::spawn?

join! for a few futures that mostly wait, like network calls. tokio::spawn when the work should run as its own task, which a multi-threaded runtime can put on another worker thread.

Verified: this timeline matches a real program, checked in CI on Rust 1.99 with Tokio 1.54. How we verify. Unclear or wrong? Tell us