← Back to Frameworks

FastAPI — Async vs Sync

FrameworksMidconcurrencyfastapi

The Question

How does FastAPI handle async vs sync endpoints?

What a Strong Answer Covers

  • sync = threadpool
  • "async = event loop
  • "blocking in async = bad

Senior-Level Answer

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)

Key Differences

Aspectasync def endpointdef endpoint
Execution contextEvent loop (main thread)ThreadPoolExecutor
Concurrency modelCooperative (await yields)Preemptive (OS threads)
Safe for blocking I/ONo — blocks event loopYes — runs in thread
Sync blocking code insideDangerous — stalls serverSafe
Scalability ceilingEvent loop (thousands reqs)Thread count (~40 default)
Correct DB driverasyncpg, asyncio SQLAlchemypsycopg2, sync SQLAlchemy

What Separates a 2/3 from a 3/3

2/3 — Passing but Incomplete

Explains that async def runs in the event loop and def runs in a threadpool, with the blocking-event-loop danger clearly stated.

3/3 — Strong Answer

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.

Common Mistakes

  • Assuming sync endpoints block the event loop — FastAPI specifically avoids this via threadpool
  • Not knowing the dangerous case: async def + synchronous blocking code
  • Recommending async def for all endpoints regardless of whether blocking I/O is involved
  • Not distinguishing I/O-bound (event loop scales well) from CPU-bound (needs process pool)

Follow-Up Questions

  • What happens if you call requests.get() inside an async def FastAPI endpoint? — It blocks the event loop. All other requests stall until it returns. Use httpx.AsyncClient with await instead.
  • How many threads does FastAPI's threadpool have by default, and how do you change it? — 40 threads (Starlette default). Override via app = FastAPI() with a custom executor or set via anyio thread limiter.
  • How would you run a CPU-intensive function from a FastAPI endpoint without blocking? — await asyncio.get_event_loop().run_in_executor(ProcessPoolExecutor(), cpu_func) — offload to a process pool to bypass the GIL.
  • How does FastAPI's dependency injection behave with async vs sync dependencies? — Async dependencies must be used in async endpoints. Sync dependencies can be used in both but run in the threadpool when called from async context.

Related Questions

  • Django — Signals
  • Django — select_related vs prefetch_related
  • SQLAlchemy — Session
  • Django — Middleware
  • FastAPI — Pydantic

Can You Explain This Cold?

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
GrindQuestionsAITechnical interview assessment
TermsPrivacyAbout