Script Valley
FastAPI: Build Production Python APIs
Async Programming and DependenciesLesson 2.1

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.

Up next

How FastAPI dependency injection works with Depends

Sign in to track progress