async / await in FastAPI

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.

The Two Modes

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}
The golden rule: never call blocking I/O inside 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).

Decision Guide

SituationUse
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 sureplain def — it is always safe (threadpool), just less scalable for huge concurrency

Concurrency vs Parallelism

Calling Multiple Things in Parallel

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.

Quick Questions and Answers

Q: My async endpoint is slower than the sync version. Why?

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.

Q: Does 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.

Q: What is the default threadpool size for sync endpoints?

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.

Conclusion

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.