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.

BytExplorer 6 min read July 18, 2026

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 def when everything inside awaits — an async database driver, httpx.AsyncClient, asyncio.sleep. You're doing non-blocking I/O.
  • Use plain def when 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

  1. Everything inside is await-able and non-blocking → async def.
  2. You call requests, a sync DB driver, time.sleep, or do CPU work → plain def.
  3. Never put blocking calls directly in an async def — it freezes the event loop.
  4. Need blocking code from async? Wrap it in run_in_threadpool.
  5. 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.

Put it into practice

Stop reading, start building

This pairs with a hands-on BytExplorer course — do it on your own machine and actually keep the skill.

More in Core Concepts