Blocking Calls in Async Code
Scenario
Your FastAPI app handles 500 req/sec effortlessly. You add one endpoint that uses the standard `requests` library to fetch a 3rd-party API. Suddenly, your entire server freezes for 2 seconds every time someone hits that endpoint. All 500 users timeout.
Mental model
The event loop is a single waiter serving 100 tables. If the waiter goes to the kitchen and waits 10 minutes staring at the chef (a blocking call), no other table gets served.
Async Python runs on a single thread—the event loop. When you `await`, you tell the loop 'I am pausing, go serve someone else.' But if you make a synchronous, blocking call (like `time.sleep` or `requests.get`), the loop doesn't know you paused. It literally freezes the entire thread until the call finishes, halting all other concurrent tasks.
Deep dive
Async code achieves high concurrency by never waiting. When it hits an `await` (like `await db.fetch()`), it registers a callback and the event loop instantly switches to another request. The golden rule of async is: you must never block the event loop thread.
`await` yields control back to the event loop. Synchronous calls do not.
Standard libraries like `requests`, `time.sleep`, or `psycopg2` are synchronous. They do not yield control. If you put `requests.get()` inside an `async def` function, the event loop stops dead. It cannot process new HTTP requests, it cannot respond to open websockets, it is completely frozen until that network call returns.
A single synchronous call in an `async def` freezes the entire server for all users.
To fix this, you must either use an async-compatible library (like `httpx.AsyncClient` or `asyncpg`) and `await` it, OR, in FastAPI, declare the endpoint as a standard `def` (not `async def`). FastAPI will automatically run standard `def` endpoints in a separate background threadpool, keeping the main event loop unblocked.
Fix: Use async libraries, or let FastAPI run it in a threadpool using a standard `def`.
Code examples
The Bug: Freezing the Event Loop
from fastapi import FastAPI
import time, requests
app = FastAPI()
@app.get("/fast")
async def fast_endpoint():
return {"msg": "I am fast!"}
@app.get("/slow")
async def slow_endpoint():
# FATAL MISTAKE: Sync call inside `async def`
# The entire server freezes for 2 seconds.
# Nobody can access /fast while this is running.
time.sleep(2)
# Same issue: requests.get is blocking
# resp = requests.get("https://api.example.com")
return {"msg": "Done"}
Because it's an `async def`, FastAPI runs it directly on the event loop. `time.sleep` (or `requests.get`) blocks the thread. The event loop is stuck, meaning all other incoming requests queue up and hang.
Fix 1: Use an Async Library
import asyncio
import httpx
from fastapi import FastAPI
app = FastAPI()
@app.get("/slow-fixed-async")
async def slow_endpoint_async():
# Async sleep yields control to the loop
await asyncio.sleep(2)
# Async network call yields control
async with httpx.AsyncClient() as client:
resp = await client.get("https://api.example.com")
return {"msg": "Done, and server stayed responsive!"}
By using `await asyncio.sleep(2)` and `httpx.AsyncClient`, control is yielded back to the event loop. Other endpoints like `/fast` continue to be served instantly while this request waits.
Fix 2: Let FastAPI Threadpool It
from fastapi import FastAPI
import requests, time
app = FastAPI()
# Notice: `def`, NOT `async def`
@app.get("/slow-fixed-sync")
def slow_endpoint_sync():
# This is safe! FastAPI runs standard `def` endpoints
# in an external threadpool.
time.sleep(2)
resp = requests.get("https://api.example.com")
return {"msg": "Done, threadpool absorbed the block."}
If you MUST use a blocking library (like boto3, or an older database driver), drop the `async` keyword. FastAPI detects this and runs the function in a threadpool, preventing the main event loop from freezing.
Common mistakes
- Using `time.sleep` or sync `requests` inside `async def`: This is the classic production bug. `time.sleep` / sync `requests` / sync DB driver inside `async def` freezes the whole event loop — every request, not just this one. Fix = `asyncio.sleep`, async clients, or `def` endpoint (FastAPI threadpools it).
Recall questions
- What happens to a FastAPI server if you run `requests.get()` inside an `async def` endpoint?
- How does FastAPI handle standard `def` endpoints (without `async`) differently?
- If you are writing an `async def` endpoint, how should you make an HTTP request to a 3rd-party API?
Questions & answers
A FastAPI endpoint marked with `async def` performs a slow synchronous database query using psycopg2. What is the impact on the application under load?
The application will completely lock up under load. An `async def` function runs directly on the single event loop thread. A synchronous blocking call (like psycopg2) will freeze the loop, meaning the server cannot process any other requests for any endpoint until the query returns.
Approach: Identify that the event loop gets blocked. State the blast radius (affects ALL requests, not just the slow one).
If you absolutely must use a synchronous library like boto3 in FastAPI, how do you prevent it from blocking the event loop?
You define the endpoint function as a standard `def` (instead of `async def`), or you explicitly run the blocking code in a threadpool using `run_in_executor`. FastAPI automatically detects standard `def` endpoints and executes them in an external threadpool, keeping the main event loop free.
Approach: Give the practical framework-specific solution (dropping the async keyword) or the pure Python solution (run_in_executor).
Continue learning
Previous: Coroutines, async / await
Previous: The event loop