← Python Code Async Python
Browse Python Concepts

Common async Mistakes — Blocking the Event Loop

Mental Model

Imagine the event loop as a single, highly efficient chef managing multiple orders by constantly switching between tasks that are waiting for ingredients. If the chef suddenly decides to perform a time-consuming, personal task (a blocking call) without yielding control, all other orders will stop progress until the chef is done.

Rule: Never call blocking synchronous libraries inside async def coroutines.

The Setup

A backend developer creates an async API endpoint that processes orders and sends a confirmation email. They use the standard requests library to dispatch the email, thinking it will run concurrently with other requests.

What Does This Print?

Broken code
Python
import asyncio
import time
import requests

async def process_payment(order_id):
    print(f"Processing payment {order_id}")
    await asyncio.sleep(0.1)  # Cooperatively yields
    print(f"Payment {order_id} complete!")

async def send_confirmation_email(order_id):
    print(f"Sending email for {order_id}")
    # CRITICAL MISTAKE: Synchronous, blocking HTTP call inside async code
    response = requests.get(f"https://httpbin.org/delay/1")
    print(f"Email sent for {order_id} with status: {response.status_code}")

async def main():
    start = time.perf_counter()
    # Run payment processing and email sending concurrently
    await asyncio.gather(
        process_payment(101),
        send_confirmation_email(101)
    )
    print(f"Total pipeline execution: {time.perf_counter() - start:.2f}s")

asyncio.run(main())
Predict whether the payment processing finishes while the email is being sent, or if they execute sequentially due to the blocking call.

The Output

What actually happens
Processing payment 101 Sending email for 101 Email sent for 101 with status: 200 Payment 101 complete! Total pipeline execution: 1.15s

The event loop froze completely when executing requests.get(). While the payment task yielded control during await asyncio.sleep(0.1), the execution of send_confirmation_email blocked the single OS thread running the event loop for a full second. The payment task was starved of CPU time and could not complete its cooperative sleep until the synchronous request returned.

Why Python Does This

asyncio operates on cooperative multitasking. The event loop maintains a queue of runnable tasks and executes them sequentially on a single thread. When a task reaches an await statement, it returns control to the event loop, allowing the loop to run other scheduled tasks. However, libraries like requests or time.sleep() make blocking system calls directly on the active thread. They do not know about the asyncio loop and do not return control to it. The entire thread—and with it, the scheduler—is paused by the OS kernel, starving all other concurrent coroutines.

The Fix

Corrected pattern
Python
import asyncio
import time
import httpx  # Use an async HTTP client instead of requests

async def process_payment(order_id):
    print(f"Processing payment {order_id}")
    await asyncio.sleep(0.1)
    print(f"Payment {order_id} complete!")

async def send_confirmation_email(order_id):
    print(f"Sending email for {order_id}")
    # Correct: Use an async HTTP client and await the non-blocking call
    async with httpx.AsyncClient() as client:
        response = await client.get("https://httpbin.org/delay/1")
    print(f"Email sent for {order_id} with status: {response.status_code}")

async def main():
    start = time.perf_counter()
    await asyncio.gather(
        process_payment(101),
        send_confirmation_email(101)
    )
    print(f"Total pipeline execution: {time.perf_counter() - start:.2f}s")

asyncio.run(main())

Asynchronous code relies on cooperative multitasking, where tasks explicitly yield control using await during I/O operations. Replacing blocking synchronous calls with their await-able asynchronous counterparts (e.g., httpx.AsyncClient for requests, asyncio.sleep for time.sleep) allows the event loop to switch to other pending tasks instead of freezing.

How This Fails in Real Systems

A real-time chat application deployed on FastAPI started lagging severely during peak usage. Message latency spiked to over 15 seconds. Engineers discovered a junior developer had added a synchronous time.sleep(2) inside an async function to throttle message retries. A single retry blocked the main event loop for every user connected to that instance. The fix involved replacing it with await asyncio.sleep(2) and was pushed as a hotfix.

Key Takeaway

Never call blocking synchronous libraries inside async def coroutines.
Common mistake: Developers inadvertently call synchronous, blocking functions (like time.sleep() or requests.get()) directly inside an async def coroutine, which halts the entire event loop and negates the benefits of asynchronous programming.