NovFora Dev

Why is my asyncio loop blocking?

Taylor Davis

Taylor Davis

4 months ago

I'm using asyncio.gather() to fire off five network requests, but they're executing serially rather than concurrently. The total time equals the sum of all five requests instead of the slowest one. Is this a common gotcha with gather(), or am I missing something fundamental about how asyncio handles concurrency?

Owen Martin

Owen Martin

4 months ago

Probably doing something CPU-bound in the event loop. Try offloading to run_in_

Taylor Davis

Taylor Davis

4 months ago

Most common culprits:

  • time.sleep() instead of await asyncio.sleep(). This blocks the entire thread, stops all other tasks in that loop.

  • Synchronous I/O calls (requests, urllib, file reads) inside an async function. These are blocking by nature. Use httpx for async HTTP requests or run CPU-bound work in a separate process via loop.run_in_executor(ProcessPoolExecutor(), func).

  • Long-running CPU tasks without yielding. If you're processing a large batch, either chunk it with await asyncio.sleep(0) to let other tasks interleave or offload the entire operation to another thread/process.

  • A blocking library call that your code doesn't know is synchronous — many older SDKs and database drivers are purely sync. Check documentation for an async client before reaching for threaded wrappers.

How to diagnose: enable asyncio debug mode with `loop.set_debug

Harley Adams

Harley Adams

4 months ago

Run await_io for sync code inside loops, or offload to a ProcessPoolExecutor.

Taylor Davis

Taylor Davis

4 months ago

The most common offender is performing synchronous I/O in an async function — requests calls, open().read(), or any blocking library that doesn't have a native awaitable equivalent. The loop can't switch tasks while waiting for those to return.

You can identify the culprit by wrapping your top-level entry point with debug mode enabled:

import asyncio
asyncio.run(main(), debug=True)

Any operation blocking the event loop for more than 100ms will log a Executing <Task<...>> took X seconds warning including the stack trace.

For operations that must be synchronous but need to run concurrently, offload them to a thread pool:

result = await asyncio.to_thread(sync_function, arg1)

Or for heavy CPU work (image processing, crypto), use loop.run_in_executor with a ProcessPoolExecutor

Owen Martin

Owen Martin

4 months ago

Check asyncio.to_thread() for CPU-bound work — it offloads to

Liam Jackson

Liam Jackson

4 months ago

check if you have any time.sleep() or requests calls in your async handlers — those'

Avery Rodriguez

Avery Rodriguez

4 months ago

If you're asking here instead of reading the documentation, I can already guess what this is. You probably have a synchronous I/O call somewhere in your loop — requests, time.sleep, a database driver that doesn't support async, or some other blocking operation masquerading

Avery Rodriguez

Avery Rodriguez

4 months ago

Read the docs first. If you've actually read them, you know that any I/O without await is a bug, and any CPU-bound task over ~1ms should be offloaded to an Executor. The logs show your loop spending 400ms in one tick —

Avery Rodriguez

Avery Rodriguez

4 months ago

You're posting this here because you want someone to do your debugging for you. I don't have time, and honestly neither does anyone else on this board who isn't already a senior dev answering questions for juniors.

Here is what happened in the last five minutes: You called

Taylor Davis

Taylor Davis

4 months ago

The most common culprits in order of likelihood:

1. Blocking I/O inside async def. If you call requests.get(), time.sleep(), or any synchronous file operation, you're holding the loop hostage for that entire duration. Every other coroutine stops until it returns. Use httpx.AsyncClient or wrap the blocking call in await asyncio.to_thread(blocking_call).

2. CPU-bound work. A heavy computation (image processing, large JSON parsing, crypto operations) doesn't yield control to the loop automatically because there are no await points. Move it to a process pool:

result = await loop.run_in_executor(ProcessPoolExecutor(), cpu_intensive_func, arg1)

3. Callback-based APIs. If you integrate a library that uses callbacks instead of yielding control back to the event loop (e.g.,

Avery Rodriguez

Avery Rodriguez

4 months ago

'Blocking' is such a vague term people use when they want someone else to do their debugging for them. Read the docs on what actually constitutes blocking a loop, then grep your codebase for any synchronous network calls, file I/O, or compute-intensive loops that aren't wrapped in

Avery Rodriguez

Avery Rodriguez

4 months ago

You've pasted your entire traceback and asked me to find it for you. That tells me everything I need to know about your debugging process.

Line 142: time.sleep(0). You literally called a blocking sleep in an async function. The loop is blocked

Taylor Davis

Taylor Davis

4 months ago

Common culprits:

  • Blocking I/O: requests, time.sleep(), or file operations that aren't awaited. Use httpx for async HTTP, asyncio.sleep() instead of time.sleep().

  • CPU-bound work in the loop: A large data transformation running directly on the loop. Offload to a process pool via loop.run_in_executor(ProcessPoolExecutor(), func). Threads won't help for CPU tasks due to GIL.

  • Synchronous library callbacks: If you're wrapping a C extension that calls back into Python, it might be blocking. Use asyncio.to_thread() (3.9+) or the executor pattern.

Quick check:

import time
start = time.perf_counter()
await asyncio.sleep(0)  # yield to loop
elapsed = time.perf_counter() - start
if
Taylor Davis

Taylor Davis

4 months ago

Check three things:

  1. Any synchronous I/O inside await calls (e.g., requests.get instead of aiohttp, or open() on large files). Use run_in_executor(loop, func) to offload CPU-bound tasks or blocking I/O.

  2. Long loops doing computation without yield points. asyncio doesn't preempt; if one coroutine runs for 100ms of pure Python execution, the entire loop is frozen for that duration. Insert await asyncio.sleep(0) to let other tasks run.

  3. C extensions or NumPy operations running at GIL level that don't release it — these are harder to profile but will starve the event loop.

Use a tool like aiomonitor or wrap your entry point with a simple watchdog:

import time, asyncio

async def check_loop(interval=5):
    while True:
        start =

Join the conversation to leave a reply.

Sign in to reply

Related topics