Rust Tokio Blocking
Blocking work is code that holds a thread until it finishes, like a heavy calculation or a normal file read. Blocking work on a runtime thread makes every other task on it wait.
The blocking way: work on the worker thread
This loop runs inside an ordinary task. While it counts, the worker thread can do nothing else โ every task scheduled on it waits:
let mut sum: u64 = 0;
for n in 1..=1_000_000u64 { sum += n % 7; }
println!("sum = {sum}");The same work, moved to the blocking pool
spawn_blocking runs the closure on a spare thread from the blocking pool instead. It returns a JoinHandle, just like spawn โ await it the same way:
let heavy = tokio::task::spawn_blocking(|| {
let mut sum: u64 = 0;
for n in 1..=1_000_000u64 { sum += n % 7; }
sum
});
println!("blocking result = {}", heavy.await.unwrap());A blocking file read moves the same way
std::fs is blocking too. Wrap the call โ or use tokio::fs, its async twin:
// blocking std::fs, wrapped so it runs on the blocking pool:
let contents = tokio::task::spawn_blocking(|| std::fs::read_to_string("notes.txt")).await;Example
// Cargo.toml: tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
let light = tokio::spawn(async { "quick task done" });
let heavy = tokio::task::spawn_blocking(|| {
let mut sum: u64 = 0;
for n in 1..=1_000_000u64 { sum += n % 7; }
sum
});
println!("{}", light.await.unwrap());
println!("blocking result = {}", heavy.await.unwrap());
}Key points
- Never block a runtime thread: no long calculations, no thread::sleep, no blocking file or network calls
- The blocking pool is large, with 512 threads by default.
- But it is not endless.
- It is for waiting-style work, not thousands of CPU jobs.
- For heavy pure-CPU work in a long-running program, a crate like rayon is often the better tool
- tokio::time::sleep pauses a task, not a thread.
- Blocking work belongs on the blocking pool, not on a runtime thread.
In this example, the number-crunching loop runs inside spawn_blocking, on a thread from the blocking pool. The light task still runs on a normal worker thread and finishes on its own schedule. Awaiting the blocking handle gives 2999998. If the loop had run inside a normal task, it would have hogged a worker thread for the whole count.
Note
std::thread::sleep pauses the whole thread โ a classic async bug.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท Rust Playground
Exercise
A task must read a file with std::fs, the blocking API. What should you do?