How to Prevent Tokio from Dropping Tasks When Threads Are Exhausted

This article explains why Tokio may discard asynchronous tasks when the thread pool is saturated and demonstrates how to use a bounded JoinSet to block task creation until resources become available, ensuring all tasks complete.

JD Cloud Developers
JD Cloud Developers
JD Cloud Developers
How to Prevent Tokio from Dropping Tasks When Threads Are Exhausted

Tokio is the most popular asynchronous runtime in the Rust ecosystem. While its non‑blocking nature gives excellent performance, in real projects you may need to block task creation when resources are scarce.

Problem demonstration

The following program creates a multi‑threaded runtime with only one worker thread and spawns 100 asynchronous tasks. The expected behavior is that each task prints “spawn i”, but the actual output shows that many spawn messages are missing because Tokio discards tasks when the thread pool is saturated.

fn main(){
    let max_task = 1;
    let rt = runtime::Builder::new_multi_thread()
        .worker_threads(max_task)
        .build()
        .unwrap();
    rt.block_on(async {
        println!("tokio_multi_thread ");
        for i in 0..100 {
            println!("run {}", i);
            tokio::spawn(async move {
                println!("spawn {}", i);
                thread::sleep(Duration::from_secs(2));
            });
        }
    });
}

The observed output lists many “run” lines but only a subset of “spawn” lines, stopping after about 59 spawns.

Solution with a bounded task set

By using JoinSet we can limit the number of concurrent tasks. The runtime is created with the desired number of worker threads, and before spawning a new task we wait until the set’s length drops below the limit.

fn main(){
    let max_task = 2;
    let rt = runtime::Builder::new_multi_thread()
        .worker_threads(max_task)
        .enable_time()
        .build()
        .unwrap();
    let mut set = JoinSet::new();
    rt.block_on(async {
        for i in 0..100 {
            println!("run {}", i);
            while set.len() >= max_task {
                set.join_next().await;
            }
            set.spawn(async move {
                sleep().await;
                println!("spawn {}", i);
            });
        }
        while set.len() > 0 {
            set.join_next().await;
        }
    });
}

This approach ensures that at most max_task tasks are active simultaneously; additional tasks block until a slot becomes free, preventing Tokio from dropping work.

The final output shows interleaved “run” and “spawn” lines for all 100 iterations, confirming the expected behavior.

Feel free to try the code yourself.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

concurrencyRustTokioThreadPoolJoinSet
JD Cloud Developers
Written by

JD Cloud Developers

JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.