If your endpoint only calls synchronous functions — requests, synchronous SQLAlchemy, or CPU-bound work — declare it with def. FastAPI runs it in a threadpool, so it never blocks other requests. Only use async def when there is a real await inside, such as calling httpx.AsyncClient or an async database. This rule applies to every endpoint.
The Problem
Many FastAPI beginners think “async is always faster”, so they declare every endpoint with async def and then call requests.get(), time.sleep(), or a synchronous SQLAlchemy query inside. One slow request blocks the entire event loop, and the other requests have to queue up even though the server still has CPU to spare — and this bug is hard to spot, because running the endpoint on its own still returns the correct result.
from fastapi import FastAPIimport requestsapp = FastAPI()@app.get("/users/{user_id}")async def get_user(user_id: int): # LỖI: requests.get() là hàm đồng bộ, chặn cả event loop response = requests.get(f"https://api.example.com/users/{user_id}") return response.json()
While this request waits for the external API, no other request gets processed.
The Solution
Split your endpoints according to the nature of the library inside them:
from fastapi import FastAPIimport requestsimport httpxapp = FastAPI()@app.get("/users/{user_id}")def get_user(user_id: int): # ĐÚNG: def thường -> FastAPI chạy trong threadpool response = requests.get(f"https://api.example.com/users/{user_id}") return response.json()@app.get("/posts/{post_id}")async def get_post(post_id: int): # ĐÚNG: async def -> chỉ khi có await thật sự async with httpx.AsyncClient() as client: response = await client.get(f"https://api.example.com/posts/{post_id}") return response.json()
FastAPI inspects the function type when registering a route: a def endpoint is pushed to a threadpool (40 threads by default), while an async def endpoint runs directly on the event loop. I measured this on uvicorn: 4 concurrent requests to a def endpoint using time.sleep(0.5) finished in 0.56 seconds; 2 requests to an async def endpoint using time.sleep(0.5) took 1.01 seconds because they ran one after another; while an async def endpoint using await asyncio.sleep(0.5) finished 4 requests in 0.51 seconds.
Quick selection rule:
- Synchronous libraries (
requests,psycopg2, synchronous SQLAlchemy, file reading, CPU processing) → usedef. - Async libraries (
httpx.AsyncClient,asyncpg, async SQLAlchemy,aiofiles) → useasync def+await. - Dependencies follow exactly the same rule.
Putting It Into Practice
Audit the async def endpoints in your project: look for calls to requests., time.sleep(, or synchronous .query( and .execute( with no await — switching them to def is all it takes, with no logic changes needed. For new endpoints, decide by the library: if the library offers an AsyncClient, use async def; if not, just use def. Don’t convert everything to async just because you heard “async is faster” — async only pays off when you actually await while waiting on I/O.
Related Posts
- FastAPI: handle heavy work in the background with BackgroundTasks, so the client doesn’t have to wait
- FastAPI: manage DB sessions safely with a yield dependency in 5 minutes
- FastAPI: export large CSV files with StreamingResponse, so the server no longer loads everything into RAM
FAQ
Is `def` slower than `async def` in FastAPI?
Not noticeably for synchronous I/O work: a def endpoint runs in a threadpool, so it still handles many requests in parallel. Async only pulls ahead when many connections wait on I/O at once and you await correctly.
What happens if an `async def` endpoint calls a synchronous function?
It blocks the event loop until that function finishes, and every other request waits. If you can’t switch it to def, wrap the call with await run_in_threadpool(...) from fastapi.concurrency.
Can a `def` endpoint call async code?
Not directly, because def can’t use await. The simplest fix is to convert the endpoint to async def; only use asyncio.run() when truly necessary, since it creates a new event loop.
Do dependencies have to follow this rule too?
Yes: a def dependency runs in the threadpool, an async def dependency runs on the event loop. The yield-dependency post on managing DB sessions already follows this rule.
How can I tell an endpoint is blocking the event loop?
The clearest sign: one slow request slows down every other endpoint. Measure it with a request-timing middleware, or fire many concurrent requests with httpx and compare the total time.
💬 Have you tried this approach in your own project? If you hit an error or know a shorter way, leave a comment below — I read them all and will fold the best ideas into the post.
Sources
- FastAPI — Concurrency and async / await: https://fastapi.tiangolo.com/async/
- AnyIO — Threads: https://anyio.readthedocs.io/en/stable/threads.html
