FastAPI Async and Await
FastAPI supports both regular and asynchronous route functions. Understanding when to use async def versus def makes a real difference in how efficiently your API handles many simultaneous requests.
The Restaurant Kitchen Analogy
Synchronous chef (def): Takes order 1 → cooks → serves → takes order 2 → cooks → serves While cooking order 1, the chef stands idle waiting for the oven. Order 2 waits the entire time. Asynchronous chef (async def): Takes order 1 → puts in oven → takes order 2 → puts in oven While both ovens are running, takes order 3. Serves each dish the moment it is ready. Same chef, far more customers served per hour.
The Core Concept: Waiting Without Blocking
Synchronous — blocks while waiting:
result = database.query() ← thread sits idle for 50ms waiting
return result
Asynchronous — frees the thread while waiting:
result = await database.query() ← thread handles other requests
during the 50ms wait
return result
The await keyword says: "Start this operation. While it is waiting for the network or disk, go serve other requests. Come back here when the result is ready."
def vs async def in FastAPI
@app.get("/sync-route")
def sync_route():
# Regular Python function
# FastAPI runs this in a thread pool
# Safe for blocking calls (like regular SQLAlchemy queries)
return {"type": "synchronous"}
@app.get("/async-route")
async def async_route():
# Async Python function
# Runs on the event loop
# MUST only use awaitable (async-compatible) operations inside
return {"type": "asynchronous"}
When to Use async def
Use async def when your route calls async libraries: ✓ httpx (async HTTP client) ✓ asyncpg or databases (async database drivers) ✓ aiofiles (async file I/O) ✓ WebSocket operations ✓ Any function you call with await Use regular def when using synchronous libraries: ✓ SQLAlchemy (synchronous ORM) ✓ requests library ✓ Standard file I/O ✓ CPU-heavy calculations
The Danger: Blocking Inside async def
# WRONG — blocks the event loop:
@app.get("/bad")
async def bad_route():
time.sleep(5) ← blocks ALL requests for 5 seconds
return {}
# CORRECT — use asyncio.sleep for async waiting:
import asyncio
@app.get("/good")
async def good_route():
await asyncio.sleep(5) ← only pauses this route, others run freely
return {}
# WRONG — calling blocking DB inside async def:
@app.get("/users")
async def get_users():
users = db.query(User).all() ← synchronous call blocks the event loop
return users
# CORRECT — use regular def with synchronous SQLAlchemy:
@app.get("/users")
def get_users():
users = db.query(User).all() ← FastAPI runs this in a thread pool
return users
Making Multiple Async Calls in Parallel
Use asyncio.gather() to run several async operations at the same time instead of one after another:
import asyncio
import httpx
@app.get("/dashboard")
async def dashboard():
async with httpx.AsyncClient() as client:
# Run both requests at the same time
user_task = client.get("https://api.example.com/users")
product_task = client.get("https://api.example.com/products")
user_res, product_res = await asyncio.gather(user_task, product_task)
return {
"users": user_res.json(),
"products": product_res.json()
}
Sequential (slow):
fetch users → wait 300ms → fetch products → wait 300ms = 600ms total
Parallel (fast):
fetch users ──────────────────┐
fetch products ───────────────┤ both finish at ~300ms = 300ms total
▼
combine results
How FastAPI Handles Both Types
async def route → runs on event loop (single thread, non-blocking) def route → runs in thread pool (multiple threads, blocking allowed) FastAPI manages both automatically. You choose the right one based on your library.
Key Points
- Use
async deffor routes that call async libraries (httpx, asyncpg, WebSockets). - Use regular
deffor routes that use synchronous libraries like SQLAlchemy. - Never call blocking operations (like
time.sleep) insideasync def— it freezes all requests. - Use
asyncio.gather()to run multiple async operations in parallel and halve the wait time. - FastAPI automatically runs
defroutes in a thread pool so they do not block the event loop.
