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
- join!:
tokio::join!(fetch("a"), fetch("b"))polls both futures. Whenahit its.await,join!startedb. Both calls overlapped, and it took about 100 ms.
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