Python for JS Devs · FastAPI
The #1 FastAPI mistake — and the one your JS event-loop instincts let you understand faster than most Python devs.
Recall Lesson 10: one event loop, one thread, and blocking work freezes it. This lesson is that law applied to FastAPI — and it's exactly the bug in the AI-generated code you started with.
@router.post("/register")
async def register_user(user: RegisterUser, db: Session = Depends(get_db)):
if db.query(User).filter(...).first(): # ⚠️ BLOCKING call…
... # …inside an async def
async def tells FastAPI "this runs on the event loop." But db.query(...) is a synchronous, blocking DB call. While it waits on the database, it blocks the entire loop — every other request is frozen until it returns. You get the worst of both worlds: async ceremony, zero concurrency, and worse throughput under load.
| Your handler does… | Declare it… | Because |
|---|---|---|
awaits async libs only (httpx.AsyncClient, async DB) | async def | Cooperates with the loop |
Blocking/sync I/O (sync SQLAlchemy, requests, file I/O) | def | FastAPI runs it in a threadpool |
| Nothing I/O-bound, pure quick logic | either | Doesn't matter much |
def (not async), FastAPI automatically runs it in a threadpool, so a blocking DB call there does not freeze the loop. So the fix for the buggy code is often just: drop the async. Sync route + sync DB = correct. Async route + async DB = correct. The trap is only async route + sync blocking call.
@router.post("/register")
def register(user: RegisterUser,
db: Session = Depends(get_db)):
# runs in threadpool; loop stays free
...@router.post("/register")
async def register(user: RegisterUser,
db: AsyncSession = Depends(get_db)):
await db.execute(...) # awaited, non-blockingasync def, don't call time.sleep() (use await asyncio.sleep()), don't use the sync requests library (use httpx.AsyncClient), and push heavy CPU work to run_in_executor / a worker. Same discipline as never blocking Node's loop.
Write two routes: @app.get("/block") as async def that calls time.sleep(5), and @app.get("/ok") that returns instantly. Hit /block then immediately /ok in another tab — /ok is frozen for 5s. Now change /block to plain def and repeat — /ok responds instantly. Feeling the loop freeze and unfreeze is the win, and it locks in the rule forever.
async def route will:def route with blocking I/O is handled by FastAPI via:def routes to a threadpool, keeping the loop free.await asyncio.sleep() yields to the loop; time.sleep() blocks it.