tokio-blocked

Crates.iotokio-blocked
lib.rstokio-blocked
version0.1.0
created_at2025-08-21 14:00:21.044662+00
updated_at2025-08-24 11:42:17.474128+00
descriptiontracing layer that logs tokio tasks that are blocked for too long - helps find synchronous or CPU heavy code in async tokio code.
homepage
repositoryhttps://github.com/theduke/tokio-blocked
max_upload_size
id1804885
size49,768
Christoph Herzog (theduke)

documentation

README

tokio-blocked

Crates.io Docs.rs License: MIT/Apache-2.0

tokio-blocked integrates with Tokio's tracing feature to detect tasks that are blocked by synchronous or CPU-heavy code, and surfaces information to developers through log messages or data dumps.

Why?

One of the most common mistakes in async Rust code is running synchronous blocking operations or CPU-heavy code inside async tasks.

The general recommendation is to offload all code that takes more than 10–100 microseconds by using tokio::task::spawn_blocking.

Not doing this can lead to mysterious latency spikes, stalls, and degraded performance that only show up under load or in production.

Quickstart

If you prefer examples, just jump into the example directory and execute the ./run.sh script.

NOTE: The tracing feature in Tokio is experimental (as of Tokio 1.47). To enable it, set the environment variable RUSTFLAGS="--cfg tokio_unstable" when building.

To use tokio-blocked, follow these steps:

In Cargo.toml:

[dependencies]

# Enable the tracing feature for Tokio
tokio = { version = "1", features = ["tracing", "rt-multi-thread", "macros"] }

# Depend on tokio-blocked and the tracing crates
tokio-blocked = "*"

tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["fmt", "env-filter"] }

In main.rs:

use std::time::Duration;
use tokio_blocked::TokioBlockedLayer;
use tracing_subscriber::{
    EnvFilter, Layer as _, layer::SubscriberExt as _, util::SubscriberInitExt as _,
};

#[tokio::main]
async fn main() {
    // Prepare the tracing-subscriber logger with both a regular format logger
    // and the TokioBlockedLayer.

    {
        let fmt = tracing_subscriber::fmt::layer().with_filter(EnvFilter::from_default_env());

        let blocked = TokioBlockedLayer::new()
            .with_warn_busy_single_poll(Some(Duration::from_micros(150)));

        tracing_subscriber::registry()
            .with(fmt)
            .with(blocked)
            .init();
    }

    tokio::task::spawn(async {
        // BAD!
        // This produces a warning log message.
        std::thread::sleep(Duration::from_secs(2));
    })
    .await
    .unwrap();
}

Now the code can be run with:

RUSTFLAGS="--cfg tokio_unstable" RUST_LOG=warn cargo run

You will see a log message like this:

2025-08-23T06:40:30.860946Z  WARN tokio_blocked::task_poll_blocked: poll_duration_ns=2000394057 callsite.name="runtime.spawn" callsite.target="tokio::task" callsite.file="src/main.rs" callsite.line=24 callsite.col=5

Configuration

TODO

Notes and Limitations

  • Blocking code location Tokio can only capture the location of the tokio::task::spawn callsite, so that is all the information tokio-blocked can provide. If you have a large future that can take many different code paths, like in a web server with many routes, it can be very hard to find the exact location of the blocking code.

    A workaround is to wrap potentially problematic code in tokio::spawn(...).await no narrow down the location.

Develop

Acknowledgements

Thanks to tokio-console for examples of extracting information from the Tokio trace data.

License

Licensed under either of:

  • Apache License, Version 2.0 (LICENSE-APACHE)
  • MIT license (LICENSE-MIT)

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Commit count: 17

cargo fmt