FastAPI: async def vs def — When to Use Each
FastAPI lets you write endpoints as either `async def` or plain `def`, and picking wrong can quietly tank your performance. The rule is simpler than the internet makes it sound.
FastAPI accepts both, and both "work":
@app.get("/a")
async def handler_a(): ...
@app.get("/b")
def handler_b(): ...
So which do you use? The wrong choice doesn't throw an error — it just silently serialises your requests, so your fast API handles one user at a time under load. Here's the actual rule.
The one rule
- Use
async defwhen everything insideawaits — an async database driver,httpx.AsyncClient,asyncio.sleep. You're doing non-blocking I/O. - Use plain
defwhen you call blocking libraries —requests, a synchronous DB driver,time.sleep, heavy CPU work, most file I/O.
Why it matters: FastAPI runs async def handlers on the event loop, and it runs plain def handlers in a threadpool. The event loop is a single thread. If you put blocking code in an async def, it freezes the loop and every other request waits.
The trap: blocking code inside async def
import time, requests
@app.get("/bad")
async def bad():
time.sleep(2) # ❌ blocks the whole event loop for 2s
return requests.get("...") # ❌ blocking HTTP in an async handler
While this sleeps, no other request on the server progresses — not just this one. Ten concurrent callers take 20 seconds total. The fix is either make it truly async, or make it a plain def:
@app.get("/good-sync")
def good_sync():
time.sleep(2) # fine — runs in a threadpool, doesn't block the loop
return requests.get("...").json()
import httpx
@app.get("/good-async")
async def good_async():
async with httpx.AsyncClient() as client:
r = await client.get("...") # non-blocking, event-loop-friendly
return r.json()
If you must call blocking code from an async handler
Push it off the loop with a threadpool helper:
from fastapi.concurrency import run_in_threadpool
@app.get("/mixed")
async def mixed():
result = await run_in_threadpool(blocking_function, arg)
return result
Which is faster?
Neither, inherently. async shines when handlers spend their time waiting on I/O — it lets one thread juggle thousands of waiting requests. For CPU-bound work, async gives you nothing (and blocks the loop); a threadpool def — or a separate worker/queue — is the right tool.
The checklist
- Everything inside is
await-able and non-blocking →async def. - You call
requests, a sync DB driver,time.sleep, or do CPU work → plaindef. - Never put blocking calls directly in an
async def— it freezes the event loop. - Need blocking code from async? Wrap it in
run_in_threadpool. - Reach for async DB/HTTP drivers (
httpx, async SQLAlchemy) to make handlers genuinely async.
Frequently Asked Questions
Is async def always faster in FastAPI?
No. async def only helps when the handler waits on non-blocking I/O. For CPU-bound work or blocking libraries, async gives no speedup and can hurt by blocking the event loop — a plain def (which runs in a threadpool) is safer there.
What happens if I use requests inside an async def?
requests is blocking, so it stalls the single-threaded event loop until it returns — every other in-flight request waits too. Use httpx.AsyncClient with await, or make the endpoint a plain def.
Can I mix async def and def endpoints in one app?
Yes. FastAPI handles each correctly — async def on the event loop, def in a threadpool. Choose per endpoint based on whether its work is awaitable or blocking.
Stop reading, start building
This pairs with a hands-on BytExplorer course — do it on your own machine and actually keep the skill.