Your FastAPI endpoint responds in 20ms when you test it alone. Then real traffic arrives and latency climbs to seconds. Nothing in your code got slower. So what happened?
The Symptom
You have an async def endpoint that calls a library like requests or a synchronous database driver:
import requests @app.get("/report") async def report(): data = requests.get("https://api.example.com/data").json() # blocking! return {"data": data}
Under one user this is fine. Under fifty concurrent users, every request queues behind every other.
Why It Happens
FastAPI runs async def endpoints on a single event loop. The loop can juggle thousands of tasks โ but only if each task yields while it waits. requests.get() never yields. It blocks the entire event loop until the network call returns. While it blocks, no other request can make progress.
You put a synchronous, blocking call inside a cooperative, single-threaded loop. Concurrency collapses to one-at-a-time.
The Fix
Option A โ use an async client. Replace blocking libraries with async ones so the call yields control while waiting.
import httpx @app.get("/report") async def report(): async with httpx.AsyncClient() as client: resp = await client.get("https://api.example.com/data") return {"data": resp.json()}
Now the event loop is free to serve other requests during the await.
Option B โ make the endpoint synchronous. If you must call blocking code, define the endpoint with plain def. FastAPI then runs it in a thread pool, keeping the event loop unblocked.
@app.get("/report") def report(): # note: def, not async def data = requests.get("https://api.example.com/data").json() return {"data": data}
The rule: async def endpoints must only await non-blocking calls. Anything blocking belongs in a def endpoint or a thread executor.
Prevention
- โธAudit every
async deffor synchronous I/O: file reads,time.sleep, sync DB drivers,requests. - โธUse
asyncio.sleepinstead oftime.sleepinside async code. - โธLoad-test with concurrency, not a single request โ the problem is invisible otherwise.
The takeaway: async is not automatically faster. A single blocking call inside the event loop serializes your whole app. Match blocking work to threads and async work to await.