How to separately measure time spent suspended and blocking for a given coroutine in python?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
In asyncio, a coroutine can spend time actively running Python code, suspended while awaiting something else, or blocking the event loop by calling synchronous code that should not have been there. Measuring those buckets separately is useful, but it requires instrumentation because asyncio does not automatically expose a neat built-in counter for each state per coroutine.
What You Can and Cannot Measure Directly
A coroutine does not carry a built-in stopwatch for “suspended time” and “blocking time.” You have to infer those durations from how the coroutine is written.
A practical model is:
- Active time: time spent executing coroutine code between
awaitpoints - Suspended time: time spent inside awaited asynchronous operations
- Blocking time: time spent in synchronous operations that stall the event loop, such as
time.sleep()or CPU-heavy work
The important limitation is that you can accurately measure these categories only where you add instrumentation.
Measure Suspended Time Around await
A simple pattern is to wrap awaited operations and accumulate how long the coroutine stays suspended while waiting for them.
Now use it inside a coroutine:
This records time spent waiting asynchronously separately from time spent in explicitly measured blocking calls.
Why time.sleep() Is Different from asyncio.sleep()
The distinction matters a lot:
- '
await asyncio.sleep(1)suspends the coroutine and gives control back to the event loop.' - '
time.sleep(1)blocks the thread and prevents the event loop from running other coroutines.'
That means identical wall-clock durations have very different performance implications. Measuring them separately helps you spot accidental blocking inside async code.
Estimate Active Execution Time Too
If you want a fuller picture, track total wall time and subtract the suspended and blocking portions you measured.
This is still approximate because it only counts the code paths you instrument, but it is often enough to answer practical performance questions.
Detect Hidden Blocking
The hardest part is hidden blocking, where a coroutine calls a library function that looks harmless but performs synchronous work. There is no perfect per-coroutine automatic answer, but useful techniques include:
- Enable
asynciodebug mode - Log slow callbacks
- Profile suspicious sections with
perf_counter - Move CPU-bound work into
run_in_executoror a process pool
If you need exact attribution for every awaitable and callback across a large system, you are moving from simple timing into tracing or profiling territory.
Common Pitfalls
A common mistake is measuring only total wall time and assuming any long duration means blocking. A coroutine can take a long time because it spent most of that time properly suspended on I/O.
Another mistake is thinking asyncio automatically tells you which time belonged to suspension versus blocking. It does not; you need wrappers, tracing, or profiler support.
A third mistake is leaving synchronous calls such as time.sleep() or heavy JSON parsing inside async code. Even if the measured blocking time is small in one test, it can become a system-wide bottleneck under load.
Summary
- '
asynciodoes not expose built-in per-coroutine suspended and blocking timers.' - You can measure suspended time by wrapping awaited operations.
- You can measure blocking time by timing known synchronous calls inside the coroutine.
- Total wall time can be split into approximate active, suspended, and blocking portions.
- For hidden blocking, use debug mode, profiling, and better task design rather than relying on one magic API.

