When to use async def vs def in FastAPI routes
async def vs def, event loop, blocking vs non-blocking I/O, thread pool executor, CPU-bound vs I/O-bound tasks, uvicorn worker model
async def vs def in FastAPI
FastAPI supports both def and async def route handlers, but they behave differently under the hood. Choosing the wrong one hurts performance.
Use async def for I/O-bound work
When your route calls an async library — httpx, asyncpg, aiofiles — define the handler with async def and await those calls:
import httpx
from fastapi import FastAPI
app = FastAPI()
@app.get("/weather")
async def get_weather(city: str):
async with httpx.AsyncClient() as client:
r = await client.get(f"https://api.example.com/weather?q={city}")
return r.json()
While waiting for the HTTP response, the event loop handles other requests. No threads are blocked.
Use def for blocking or CPU-bound work
If you call synchronous code — a blocking database driver, CPU-heavy computation — use plain def. FastAPI runs it in a thread pool automatically, keeping the event loop free:
@app.get("/compute")
def heavy_compute(n: int):
result = sum(i * i for i in range(n))
return {"result": result}
The mistake to avoid: using async def with blocking code (like time.sleep or a synchronous SQL driver). This blocks the entire event loop and degrades all concurrent requests to sequential.
Rule of thumb
Use async def only when calling awaitable code. Otherwise, use def and let FastAPI's thread pool handle it safely.
