async def (Event Loop) vs def (anyio Worker Thread Pool Execution)
- Last updated
- Prerequisites:
- Python asyncio event loop
- Blocking vs Non-blocking I/O
- fastapi
- async-def
- def
- event-loop
- threadpool
- anyio
- concurrency
Read at your depth
The practical view
In FastAPI, route handlers can be defined with `async def` or plain `def`. Use `async def` when calling asynchronous libraries using `await` (e.g. `httpx`, `asyncpg`). Use plain `def` for standard synchronous blocking code (e.g. `time.sleep()`, standard `requests`, synchronous database drivers like standard `psycopg2` or `sqlite3`). FastAPI automatically runs plain `def` endpoints in a separate background thread pool (`anyio.to_thread.run_sync`).
The same idea in other frameworks
`async def` (Event loop direct) vs `def` (anyio thread pool).Async controller methods vs offloading CPU/blocking work to worker threads.Spring WebFlux (Netty event loop) vs Spring MVC (Tomcat worker thread pool).Event loop execution vs `worker_threads` / `libuv` thread pool offload.Legacy vs modern
Event Loop Blocking Bug vs Safe anyio Thread Pool Execution
Synchronous blocking code inside `async def` freezes the entire server; using plain `def` offloads to worker threads safely.
async def endpoint(): time.sleep(5) # ❌ Freezes the entire server for all users!def safe_endpoint(): time.sleep(5) # ✅ NEW: Offloaded to worker threadpool automaticallyInterview gotchas
Context
Interviewers test your knowledge of thread pool saturation in FastAPI.
The mechanical answer
While plain `def` avoids blocking the event loop, it offloads to the `anyio` worker thread pool, which has a default limit of **40 worker threads**. If 100 concurrent requests hit slow synchronous `def` endpoints (each taking 1 second), 40 requests will occupy the threads, and the remaining 60 requests will queue up in the thread pool backlog, increasing latency. For high-concurrency I/O, true non-blocking `async def` with async drivers scales to thousands of concurrent requests.
Trap
Do not assume plain `def` scales infinitely; it is bounded by the worker thread pool capacity.
Context
Checks knowledge of manual thread offloading in async code.
The mechanical answer
Use `anyio.to_thread.run_sync()` or `asyncio.to_thread()` (Python 3.9+): ```python import asyncio @app.get('/process') async def process(): result = await asyncio.to_thread(legacy_blocking_function, arg1) return result ```
Trap
Never call a blocking synchronous function directly inside `async def` without `asyncio.to_thread()`.