Blocking in async code stalls every task

Read the explanation
What if

main calls a blocking sleep on the runtime's only thread. The ticker can't tick until it ends.

012345678910111213main: spawn tickerspawnmain: yield_now().awaityieldmain: print startsstartsstd::thread::sleep: blocks the threadprint endst.awaitreturntick 1sleep(20 ms).awaitready, but the thread is busytick 2sleepticks 3-5

Tap any block to see what that thread is doing.

  • Running
  • Waiting / parked
Step 1/6

One thread runs both tasks. main spawns a ticker that prints every 20 ms.

1async fn ticker() {2    for i in 1..=5 {3        println!("tick {i}");4        tokio::time::sleep(Duration::from_millis(20)).await;5    }6}7 8#[tokio::main(flavor = "current_thread")]9async fn main() {10    let t = tokio::spawn(ticker());11    tokio::task::yield_now().await; // let the ticker start12    println!("slow call starts");13    std::thread::sleep(Duration::from_millis(100)); // blocks the only thread14    println!("slow call ends");15    t.await.unwrap();16}

Explanation

What just happened

Both tasks ran on one thread. main called std::thread::sleep, which doesn’t .await: it put the whole thread to sleep. The ticker’s 20 ms were up, but there was no thread to run it on. tick 2 came only after slow call ends.

It compiles without a warning. Blocking calls look like any other function call.

Try the what-if switch

Why

An async runtime runs many tasks on few threads. That only works if every task gives the thread back at its .awaits. One blocking call holds it for everyone.

Watch out

A long loop with no .await stalls the thread the same way, even without a single blocking call in it.

Common questions

Why is std::thread::sleep bad in async code?

It blocks the whole thread, not just the task. On a current_thread runtime no other task can run until it returns: in our run the ticker didn't tick once during the 100 ms sleep.

What does spawn_blocking do?

It runs the closure on a separate thread from Tokio's blocking pool and gives you a future for the result. Awaiting it frees the async thread for other tasks.

Should I use spawn_blocking or tokio::time::sleep?

To wait, use tokio::time::sleep(..).await. Use spawn_blocking for work that has to block, like a blocking library call or file I/O with std::fs.

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