You added asyncio to your web scraper, wrapped your HTTP calls in await, and expected everything to fly. Instead, your requests still stall. One slow endpoint brings the whole event loop to a halt. You are not alone. Many intermediate and advanced Python developers hit this wall. The good news is that the problem is almost always the same: a blocking call that the event loop cannot yield. Once you know what to look for, fixing async python blocking code becomes straightforward.
Async Python only works when your code hands control back to the event loop. Any synchronous I/O, CPU-heavy computation, or misused library call can freeze the loop. The fix involves replacing blocking libraries, offloading CPU work, and using tools like `asyncio.to_thread()` to keep your application responsive.
The Real Reason Your Async Code Hangs
The asyncio event loop is a single-threaded scheduler. It runs one coroutine at a time, switching between them only at await points. If a coroutine never reaches an await, the loop cannot switch. That is the core issue.
Consider this common example:
import requests
import asyncio
async def fetch_data(url):
# This blocks the entire event loop
response = requests.get(url)
return response.json()
async def main():
urls = ["https://api.example.com/1", "https://api.example.com/2"]
tasks = [fetch_data(url) for url in urls]
await asyncio.gather(*tasks)
asyncio.run(main())
The requests.get() call is synchronous. It blocks the thread until the HTTP response arrives. While it waits, no other coroutine can run. The event loop starves. This is the most frequent cause of async python blocking code.
Common Blocking Patterns in Async Python
Blocking code hides in plain sight. Here are the three main categories you need to watch for.
Synchronous I/O Libraries
Any library that performs disk, network, or database operations without an async interface will block. This includes:
requests(usehttpxoraiohttpinstead)psycopg2(useasyncpgorpsycopgasync variant)redis-py(useredis.asyncio)- Standard
open()for file operations (useaiofiles)
CPU-Bound Computations
Even perfect async code cannot speed up CPU work. If you parse a massive JSON payload, hash a large file, or run a regex across a million lines, the loop stops. Python’s GIL does not help here, but the real bottleneck is that the CPU is busy.
Hidden Blocking in Third-Party Packages
Many popular packages wrap C extensions that release the GIL but still block the thread. Others simply do not expose async entry points. Always check the library documentation for async support.
How to Diagnose Blocking Code
You do not need to guess. Use these techniques to find the culprit.
-
Use
asyncio.create_task()with a timeout. Wrap your long-running coroutine and catchasyncio.TimeoutError. If the timeout fires, something is blocking. -
Run with
PYTHONASYNCIODEBUG=1. This flag enables extra logging that shows when the event loop is stuck. Look for messages likeTask <Task pending> got future ...without progress. -
Profile with
asyncio.run()and a custom loop. You can subclassasyncio.SelectorEventLoopand override_run_onceto log the time between iterations. A gap larger than 100ms signals a block. -
Use
asyncio.to_thread()as a test. Wrap suspicious calls inasyncio.to_thread(). If performance improves, the call was blocking.
Here is a practical example using a timeout:
import asyncio
async def safe_fetch(url):
try:
async with asyncio.timeout(2.0):
# Assume fetch_with_httpx is an async function
return await fetch_with_httpx(url)
except asyncio.TimeoutError:
print(f"Timeout on {url}")
return None
The Fix: Replace, Offload, or Wrap
Once you identify the blocking code, you have three options.
Replace the Library
The cleanest fix is to switch to an async-native library. For HTTP, use httpx with the async client or aiohttp. For databases, choose asyncpg for PostgreSQL or databases for multi-backend support.
Offload CPU Work
For CPU-heavy tasks, use asyncio.to_thread(). This runs the function in a thread pool executor, freeing the event loop.
import asyncio
import hashlib
def heavy_hash(data):
return hashlib.sha256(data).hexdigest()
async def main():
data = b"x" * 10_000_000
result = await asyncio.to_thread(heavy_hash, data)
print(result)
If you are doing many CPU-bound tasks, consider using concurrent.futures.ProcessPoolExecutor with loop.run_in_executor() for true parallelism.
Wrap with run_in_executor
For blocking I/O that has no async alternative, you can still use the default thread pool.
import asyncio
import requests
async def fetch_with_executor(url):
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, requests.get, url)
This is a band-aid. It works, but it introduces thread overhead. Replacing the library is almost always better.
Comparison of Fixes for Async Python Blocking Code
| Approach | Best For | Tradeoff |
|---|---|---|
| Replace with async library | I/O-heavy apps (web scrapers, API clients) | Requires library change, may break existing code |
asyncio.to_thread() |
CPU-bound tasks or blocking I/O without async alternative | Thread overhead, GIL limits CPU gains |
run_in_executor with process pool |
Heavy CPU work (parsing, crypto, image processing) | More complex, inter-process communication cost |
| Manual loop debugging | Finding hidden blocks during development | No runtime fix, only diagnostic |
A Systematic Process to Fix Blocking Code
Follow these steps when you encounter async python blocking code in your project.
- Reproduce the stall. Run your code with a single request. If it blocks, you have found the problem.
- Identify the blocking call. Read the traceback. Look for synchronous I/O or CPU loops inside async functions.
- Check for async alternatives. Search for an async version of the library. For databases, look for drivers with
asyncin the name. - Test the async replacement. Run the same workload. Compare total time and throughput.
- Offload if no alternative exists. Use
asyncio.to_thread()orrun_in_executor()as a fallback. - Add timeouts everywhere. Wrap all external calls in
asyncio.timeout()to fail fast instead of hanging.
Common Mistakes That Introduce Blocking
Even experienced developers make these errors. Keep them in mind.
- Using
time.sleep()inside an async function. Useawait asyncio.sleep()instead. - Calling
result = await some_coroutinewithout checking ifsome_coroutinecontains blocking code. - Mixing synchronous and async code without offloading. For example, using
pandasinside an async handler. - Forgetting that
__aenter__and__aexit__in async context managers can also block.
Expert advice: Always assume a third-party library is blocking until you verify its async support. Read the source code if needed. A single blocking call in a hot path can ruin the entire application’s throughput.
Tools and Libraries That Help in 2026
The Python ecosystem has matured. Here are some tools that make async work easier.
httpxwithAsyncClientfor HTTPaiofilesfor file operationsasyncpgfor PostgreSQLredis.asynciofor Redisanyiofor a unified async interface across librariesstructlogfor structured logging with async support
For profiling, try asyncio.run() with the asyncio debug mode or use py-spy to inspect a running process. You can also check out 10 Common Performance Bottlenecks in Python and How to Fix Them for broader performance advice.
What to Do When the Event Loop Still Freezes
Sometimes you fix all the obvious blocking calls, but the loop still stutters. In those cases, consider these deeper issues.
- C extension calls. Some C extensions release the GIL but still block the calling thread. Use
asyncio.to_thread()to be safe. - Too many file descriptors. The default asyncio event loop uses
select()on some platforms, which limits the number of concurrent connections. Switch touvloopfor better scalability. - Garbage collection pauses. Python’s garbage collector can pause the event loop. If you create many short-lived objects, GC runs more often. Use
gc.set_threshold()to tune it.
Putting It All Together
You write async Python to handle many I/O operations concurrently. The promise is real, but it breaks the moment you call a synchronous function. The solution is not to abandon async. It is to understand where control returns to the event loop and where it does not.
Start by auditing your dependencies. Replace blocking libraries with async versions. Use asyncio.to_thread() for CPU work. Add timeouts to every external call. Then test your application under load. You will see the difference.
If you are building web scrapers or API clients, pay special attention to your HTTP library. Many developers discover that their async python blocking code comes down to one import statement. Switching from requests to httpx often fixes everything.
For a deeper look at async patterns in another language, read Mastering Asynchronous Programming in JavaScript for Better Performance. The concepts translate well.
Your Next Step
Take one of your existing async projects. Run it with PYTHONASYNCIODEBUG=1. Watch for gaps. Identify the blocking calls. Replace or offload them. Measure the improvement.
Your event loop should never wait. With these techniques, it will not.