A deep dive into async Rust, explaining why threads are inefficient for I/O-bound workloads and how cooperative scheduling addresses this. Covers the difference between preemptive (Go) and cooperative (Rust) scheduling, async runtimes, and Tokio's architecture including its work-stealing event loops, two thread pools (fixed-size for async tasks, dynamically sized up to 512 threads for blocking tasks), and the critical rule of never blocking the event loop. Practical examples show tokio::spawn for async tasks and tokio::task::spawn_blocking for CPU-intensive operations like password hashing.
Table of contents
The problem with ThreadsPreemptive SchedulingCooperative SchedulingAsync RuntimesIntroducing tokioAvoid blocking the event loopsSome Closing ThoughtsQuestions this post answers
What is the difference between preemptive and cooperative scheduling in async Rust?
Preemptive scheduling (used by Go) lets the runtime manage task switching automatically with no code difference between sync and async. Cooperative scheduling (used by Rust) requires the developer to mark I/O waits with the `await` keyword, giving the runtime a signal to run other tasks. Cooperative scheduling is faster but easier to misuse — forgetting `await` or blocking the event loop can severely degrade performance. Rust developers navigating async architecture decisions track these trade-offs on daily.dev.
How does Tokio's thread pool work internally and what is the default max blocking threads limit?
Tokio maintains two thread pools: a fixed-size pool for async task executors (event loops), dispatched via `tokio::spawn`, and a dynamically sized pool for blocking tasks, dispatched via `tokio::task::spawn_blocking`. The blocking thread pool grows and shrinks based on demand and is bounded to 512 threads by default. This is configurable via `tokio::runtime::Builder::max_blocking_threads`. Teams tuning Tokio for production workloads find relevant deep-dives on daily.dev.
When should I use tokio::task::spawn_blocking instead of tokio::spawn in Rust?
Use `tokio::task::spawn_blocking` for CPU-intensive or blocking operations — such as password hashing, encryption, or file hashing — that would run longer than 10–100 microseconds. Calling such functions directly blocks the event loop and degrades the entire async runtime. `tokio::spawn` is for async tasks; `spawn_blocking` offloads work to a separate bounded thread pool so the event loop stays free. Developers building production Rust services stay sharp on async pitfalls like this through daily.dev.