<img src="resources/logo.png" alt="no-block-pls logo" width="248" height="268" />
Detect blocking work between async suspension points by instrumenting your Rust
sources and logging slow sections.
## Why this tool
Tokio console and similar tools watch futures from the runtime’s point of view.
When your whole app collapses into one mega-future (e.g., `async fn main` →
`server` → `pipeline`), a blocking spot shows up as something vague like
`SpawnLocation:main.rs:35`. Helpful, but not precise.
You can't obtain this information from the runtime alone, because runtime hands
over control to poll, and it can't see what happens inside.
`no-block-pls` walks your source tree, finds every `async` function, and times
the
synchronous sections between `await`s. It measures from function entry
to the first await, then from one await boundary to the next, so you see exactly
which chunk of sync work is slow.
`Instant::now()` takes 100 cycles, so overhead is negligible compared to
other async work.
It would be great to have a compiler plugin that does this to add
instrumentation during
`async fn -> state machine` conversion, but that’s not possible (yet?)
## Why not this tool
- I've tested it on a real codebase but might miss some edge-case Rust syntax.
- It patches your code; you can’t flip it on/off like tokio console.
- It can’t instrument third-party crates unless you vendor them (untested).
- Logs are hardcoded to `tracing::warn!`, it can be changed, but it's not here.
## Install
```bash
cargo install --git https://github.com/0xdeafbeef/no-block-pls
# or
cargo install no-block-pls
```
## Default workflow
```bash
# instrument in-place (creates .rs.bak backups next to originals)
no-block-pls -i
# run your app as usual and watch logs
cargo run --release
# restore backups if needed
no-block-pls -r
```
Example log output:

What you'll see when a section blocks (10 ms default):
```
WARN long poll elapsed_ms=237 name=my_crate::handlers::fetch_and_process span=src/handlers.rs:12-18 hits=1 wraparound=false
```
## How it works
- Injects a guard module into `lib.rs`/`main.rs`.
- Rewrites every async function to start a guard, pause it before each await,
and resume afterward.
- Logs any synchronous section that exceeds the threshold (10 ms by default).
### Transform example
Input:
```rust
async fn fetch_and_process() {
let data = fetch().await;
process(data);
}
```
Instrumented:
```rust
async fn fetch_and_process() {
let mut __guard = crate::__async_profile_guard__::Guard::new(
concat!(module_path!(), "::", stringify!(fetch_and_process)),
file!(),
1u32,
);
let data = {
__guard.end_section(2u32);
let __result = fetch().await;
__guard.start_section(2u32);
__result
};
process(data);
}
```
## Tips
- Use a fresh branch: instrumentation touches all async functions and writes
`.rs.bak` backups.
- To restore files, move `*.rs.bak` back over the originals.
- The logs come from `tracing::warn!`; set your subscriber accordingly.
- If a log fires only at the function end, the slow part is likely a `Drop`
impl.