The most common async mistake is reaching for it when threads would be simpler — or vice versa. This lesson is the decision framework: when async, when threads, and how to tell.
The core distinction
I/O-bound work (waiting on network, disk, timers) suits async: tasks spend most of their time waiting, so one thread can juggle thousands by switching at each await. CPU-bound work (number crunching, parsing, compression) suits threads: there's no waiting to yield on, so you want real parallelism across cores.
The trap: blocking in async
A CPU-heavy or blocking operation inside an async task is a footgun: it doesn't yield, so it starves every other task on that thread. If you must do blocking work in an async context, hand it to tokio::task::spawn_blocking, which runs it on a dedicated thread pool so the async tasks keep flowing. Mixing the two models carelessly is the number-one async performance bug.
spawn_blocking. The wrong pairing doesn't crash — it quietly destroys throughput, which is harder to debug than a crash.You can use both
Real systems combine them: an async web server (tokio) that offloads a CPU-heavy image resize to spawn_blocking or a thread pool. The models aren't rivals — they're tools for different shapes of work, and a mature Rust service uses each where it fits. Knowing which is which is the mark of someone who actually understands concurrency, not just the syntax.