FastAPI supports both normal def and async def endpoints, and choosing between them correctly is the single most important performance decision in a FastAPI app.
import httpx
@app.get("/sync-mode/")
def sync_mode():
# runs in a worker thread from the threadpool (default 40 threads)
result = blocking_call() # e.g. requests.get, time.sleep, sync DB driver
return {"ok": True}
@app.get("/async-mode/")
async def async_mode():
# runs on the event loop
async with httpx.AsyncClient() as client:
result = await client.get("https://api.example.com/data")
return {"ok": True}
def endpoint: FastAPI runs it in an external thread pool (via Starlette/AnyIO). Blocking calls are OK, but concurrency is limited by the thread pool size.async def endpoint: runs on the event loop. It can handle thousands of concurrent connections — as long as it never blocks the loop.async def.
A call like time.sleep(5) or a synchronous DB query inside an async def freezes the entire event loop — the whole server stops serving all requests until it finishes. If you must call blocking code, use def (threadpool) or wrap it: await run_in_threadpool(blocking_fn) / await asyncio.to_thread(blocking_fn).
| Situation | Use |
|---|---|
| You call async libraries (httpx, asyncpg, motor, aiofiles, redis.asyncio) | async def |
| You call blocking libraries (requests, psycopg2, pandas, boto3 sync) | plain def |
| Pure CPU work (no I/O) | plain def, and offload heavy compute to a worker (Celery/RQ) or another process |
| Not sure | plain def — it is always safe (threadpool), just less scalable for huge concurrency |
uvicorn --workers 4 (or Gunicorn managing Uvicorn workers) to use multiple CPU cores. Typically workers = 2 * cores + 1.import asyncio, httpx
@app.get("/dashboard/")
async def dashboard():
async with httpx.AsyncClient() as client:
user, orders = await asyncio.gather(
client.get("https://api.example.com/user/1"),
client.get("https://api.example.com/orders?user=1"),
)
return {"user": user.json(), "orders": orders.json()}
asyncio.gather issues both HTTP calls at the same time — total latency is the slower call, not the sum. This is where async shines.
A: Almost always because it is doing blocking work on the event loop (sync DB driver, requests, CPU-heavy pandas). The event loop serializes everything behind those calls. Switch to async libraries or declare the endpoint as plain def.
async def make my endpoint run in parallel with others automatically?A: It allows interleaved execution at every await point. Two async endpoints progress concurrently while they await I/O, but the code between awaits still runs one at a time on the single loop thread.
A: AnyIO's default is 40 threads. You can tune it: anyio.to_thread.current_default_thread_limiter().total_tokens = 100 at startup. Beyond that, sync requests queue up.
Use async def with async libraries to maximize concurrency; use plain def when you call blocking code, and let FastAPI offload it to the threadpool. Mixing them wrongly (blocking inside async def) is the #1 FastAPI performance bug.