How does FastAPI handle async vs sync endpoints?
FastAPI is built on Starlette and uses Python's `asyncio` event loop via Uvicorn (or Hypercorn) as the ASGI server. How it handles async vs sync endpoints has significant performance implications.
**Async endpoints (`async def`):**
Async endpoints run directly in the event loop on the main thread. When an async endpoint awaits an I/O operation (database query, HTTP call), it yields control back to the event loop, which can process other requests. This allows thousands of concurrent requests with a single process — the classic async I/O benefit.
```python @app.get('/items/{id}') async def get_item(id: int): item = await db.fetch_one(query, values={'id': id}) # yields to event loop return item ```
Critical rule: **you must never block the event loop in an async endpoint.** Calling synchronous blocking code (`time.sleep`, synchronous `requests.get`, `psycopg2` queries) in an async context freezes the entire server — no other requests can be processed until the blocking call returns.
**Sync endpoints (`def`):**
FastAPI detects that an endpoint is defined with `def` (not `async def`) and automatically runs it in a **threadpool** using `asyncio.run_in_executor` (backed by `concurrent.futures.ThreadPoolExecutor`). This prevents blocking the event loop.
```python @app.get('/items/{id}') def get_item(id: int): item = db.execute(query) # sync blocking — safe because it runs in threadpool return item ```
The default threadpool has 40 threads (Starlette default). Each sync endpoint occupies one thread for the duration of the call. Under high concurrency, thread pool exhaustion becomes the bottleneck.
**The dangerous case:** An `async def` endpoint that calls synchronous blocking code. FastAPI cannot detect this and will not move it to a threadpool. The blocking call runs in the event loop and stalls all concurrent requests.
**Practical guidance:** - Use `async def` with an async database driver (asyncpg, databases, SQLAlchemy async) for I/O-bound work - Use `def` with synchronous drivers (psycopg2, SQLAlchemy synchronous) — FastAPI handles threadpool dispatch - Use `asyncio.get_event_loop().run_in_executor(None, cpu_bound_func)` or a process pool for CPU-bound work — never block the event loop with CPU work in an async endpoint - FastAPI's dependency injection respects this — async dependencies in async endpoints, sync dependencies in sync endpoints (though mixing is supported with limitations)
| Aspect | async def endpoint | def endpoint |
|---|---|---|
| Execution context | Event loop (main thread) | ThreadPoolExecutor |
| Concurrency model | Cooperative (await yields) | Preemptive (OS threads) |
| Safe for blocking I/O | No — blocks event loop | Yes — runs in thread |
| Sync blocking code inside | Dangerous — stalls server | Safe |
| Scalability ceiling | Event loop (thousands reqs) | Thread count (~40 default) |
| Correct DB driver | asyncpg, asyncio SQLAlchemy | psycopg2, sync SQLAlchemy |
Explains that async def runs in the event loop and def runs in a threadpool, with the blocking-event-loop danger clearly stated.
Covers event loop vs threadpool dispatch, quantifies threadpool size, explains the dangerous async def + sync blocking antipattern, discusses CPU-bound work separately, and knows async DB driver requirements.
Reading the answer is step one. Explaining it unprompted — under interview pressure — is what actually matters. Get AI-graded feedback on your answer with follow-up probes on your weak points.
Get Graded — Free Assessment