Skip to content

Commit 2ba17df

Browse files
committed
decisions.md: state the second half of the async def rule — blocking segments inside async handlers stay in run_in_threadpool
1 parent d777b8d commit 2ba17df

1 file changed

Lines changed: 7 additions & 8 deletions

File tree

docs/decisions.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ The fix converted those three to `def`. The Turnstile session handlers stay `asy
1919
because they genuinely `await` an `httpx` coroutine — real non-blocking I/O that belongs
2020
on the event loop. That distinction is the invariant: `async def` only when the handler
2121
has 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

2327
Concurrency 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,
3945
exception handlers) are required to be async by FastAPI/Starlette's own API and are
4046
not 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

Comments
 (0)