@@ -19,6 +19,10 @@ The fix converted those three to `def`. The Turnstile session handlers stay `asy
1919because they genuinely ` await ` an ` httpx ` coroutine — real non-blocking I/O that belongs
2020on the event loop. That distinction is the invariant: ` async def ` only when the handler
2121has a real awaitable (network I/O via an async library); ` def ` for everything else.
22+ The rule has a second half: an awaitable justifies ` async def ` , but the whole body then
23+ runs on the event loop, so any blocking segment inside such a handler must itself be
24+ wrapped in ` run_in_threadpool ` . That is the wrapper's surviving role — bridging blocking
25+ portions of genuinely-async handlers, not wrapping a handler's entire body.
2226
2327Concurrency parameters: anyio threadpool sized to 80 in lifespan
2428(` anyio.to_thread.current_default_thread_limiter().total_tokens = 80 ` ); DB pool is
@@ -35,17 +39,12 @@ blocked. The same test on this branch showed 3–11 ms throughout.
3539| ` PUT /api/assignments ` | ` request.body() ` — streaming raw msgpack body |
3640| ` POST /api/commenter ` , ` /comment ` , ` /tag ` , ` /submit-comment ` | ` turnstile.verify_turnstile ` — httpx Turnstile call |
3741
38- All other route handlers are ` def ` . Non-route ` async def ` (middleware, lifespan,
42+ Each of these wraps its blocking work (sync SQLAlchemy writes, msgpack decode +
43+ assignment ingest) in ` run_in_threadpool ` ; only the awaitable itself runs on the event
44+ loop. All other route handlers are ` def ` . Non-route ` async def ` (middleware, lifespan,
3945exception handlers) are required to be async by FastAPI/Starlette's own API and are
4046not in scope of this rule.
4147
42- ** A widely circulated "FastAPI expert" skill** asserts "MUST NOT: Use synchronous
43- database operations" and "Use async/await for all I/O operations." That rule is
44- correct at the library-call level (don't call ` requests.get ` inside ` async def ` ) but
45- wrong as a handler-declaration rule. FastAPI's own documentation explicitly instructs
46- using ` def ` for blocking libraries that have no async alternative (NetworkX, sync
47- SQLAlchemy). Applying the skill's blanket rule would push handlers back to
48- ` async def ` + ` run_in_threadpool ` , reinstating the event-loop blocking this PR fixed.
4948
5049## Graphs become mmap-shared (PR #721 , merged to dev 2026-08-28)
5150
0 commit comments