FastAPI inspects the path operation function. A plain def handler is run in an AnyIO worker thread so that blocking calls do not stall the server. An async def handler runs directly on the event loop, and any blocking call inside it, a synchronous database driver, requests, time.sleep, a CPU-heavy loop, halts every other request in that worker process.
The failure mode is characteristic: throughput collapses under concurrency while a single request looks fast, and latency percentiles fan out. Enable it in a test by adding time.sleep(1) to an async def route and firing ten concurrent requests; the tenth takes ten seconds.
The rule of thumb is to write def when the body is synchronous and async def only when everything inside is awaited. Wrap unavoidable blocking work with await anyio.to_thread.run_sync(fn) or asyncio.to_thread. The same applies to dependencies, which follow the same def versus async def rule independently of the handler. The threadpool has a bounded size, so a very slow synchronous handler still limits concurrency, just less catastrophically.