Python's asyncio module utilizes a single-threaded cooperative multitasking event loop. When a coroutine executes a synchronous blocking call (such as time.sleep(), standard requests.get(), or heavy CPU-bound cryptography), execution control is not returned to the selector. Consequently, all scheduled network callbacks, heartbeats, and database connections freeze simultaneously.
[ Single-Threaded Event Loop ]
│
├── Coroutine 1 (Awaiting Network Socket) ──> [ OK ]
├── Coroutine 2 (Invokes Sync requests.get) ──> [ BLOCKS THREAD! ]
└── Coroutine 3 (Database Heartbeat Task) ──> [ FROZEN -> TIMEOUT DEADLOCK ]
Enable Python's native loop execution monitor to automatically log slow tasks during development:
import asyncio
import logging
logging.basicConfig(level=logging.DEBUG)
async def main():
loop = asyncio.get_running_loop()
# Trigger warning if any coroutine blocks the loop for > 100ms
loop.slow_callback_duration = 0.100
asyncio.run(main(), debug=True)
For live production processes experiencing silent deadlocks, attach py-spy to dump the active CPython call stack without restarting the process:
# Attach to running Python PID and dump C-stack traces
sudo py-spy dump --pid $(pgrep -f "gunicorn")
In Python 3.9+, use asyncio.to_thread() to automatically execute synchronous library calls inside a dedicated background ThreadPoolExecutor without blocking the main event loop:
import asyncio
import requests
# CORRECT: Offloads requests.get to a worker thread pool
async def fetch_user_data(user_id):
response = await asyncio.to_thread(requests.get, f"https://api.legacy.com/users/{user_id}")
return response.json()
For maximum performance under high QPS, replace synchronous HTTP clients entirely with native asynchronous drivers:
import httpx
async def fetch_user_data_async(user_id):
async with httpx.AsyncClient() as client:
response = await client.get(f"https://api.legacy.com/users/{user_id}")
return response.json()
Because CPython's Global Interpreter Lock (GIL) limits multi-threaded CPU efficiency, heavy computational tasks (such as image processing or data compression) should be offloaded to a multi-process pool:
import asyncio
from concurrent.futures import ProcessPoolExecutor
def heavy_cpu_task(data_payload):
# Heavy CPU computation running in isolated OS process
return sum(i * i for i in range(10_000_000))
async def main():
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
result = await loop.run_in_executor(pool, heavy_cpu_task, "payload")
print("Result:", result)