Course contentsShow
← Rust
Lesson 1175 of 1,91010. Concurrency PrimitivesFree lesson

Handling Blocking Tasks in Runtimes

You will offload compute-heavy work to dedicated threads to avoid starving the async reactor.

In this lesson, you will learn how to prevent CPU-intensive work from stalling your asynchronous executor and causing "reactor starvation."

The Golden Rule of Async

In an asynchronous runtime like Tokio or async-std, the executor relies on a small pool of worker threads. These threads are designed to handle non-blocking I/O—they run a bit of code, hit a wait point (like an .await), and then move on to the next task.

If you execute a heavy, blocking operation—such as a large image processing job, a synchronous file read, or a computationally expensive mathematical loop—directly inside an async fn, you essentially "trap" that worker thread. Because that thread cannot yield until your function completes, it cannot poll other pending futures. If enough tasks block, your entire system grinds to a halt, even if your server has plenty of CPU cores available.

Offloading to the Rescue

To handle this, you must offload blocking work to a dedicated thread pool designed for long-running tasks. Instead of running the work directly, you hand it off and return a Future that completes once the blocking operation finishes.

Here is how you handle a blocking task:

async fn process_data(data: Vec<u8>) -> Result<Output, Error> {
    // DO NOT do this: 
    // let result = expensive_calculation(data); // Blocks the reactor!

    // DO this:
    let result = tokio::task::spawn_blocking(move || {
        expensive_calculation(data)
    }).await?;

    Ok(result)
}

Why this works

spawn_blocking instructs the runtime to push your closure onto a separate, high-capacity thread pool. This pool is specifically tuned for threads that may run for a long time. The runtime keeps your main async worker threads lean and fast, allowing them to continue context-switching between lightweight I/O tasks while the heavy lifting happens elsewhere.

By isolating compute-heavy work, you ensure that the heartbeat of your application—the async reactor—remains responsive to incoming network requests and timers.

Key Takeaway: Never execute blocking operations directly in an async function; always offload them to a dedicated thread pool to keep the executor responsive.