Skip to content

fix: don't block the event loop on synchronous storage calls#35

Merged
Krande merged 1 commit into
mainfrom
perf/serve-offload-blocking
Jul 11, 2026
Merged

fix: don't block the event loop on synchronous storage calls#35
Krande merged 1 commit into
mainfrom
perf/serve-offload-blocking

Conversation

@Krande

@Krande Krande commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Problem

paradoc-serve runs a single uvicorn worker — one asyncio event loop. Many
route handlers are declared async def but call synchronous DocStore
methods (list_doc_ids, get_static_*, get_bundle_manifest,
put_bundle_file, …) directly. Each of those does blocking object-store I/O, so
it blocks the entire event loop for the duration of the round-trip: one slow
request stalls every other request, and the health endpoint stops responding.

The worst offender was GET /api/health, which returned
doc_store.list_doc_ids() — a synchronous object-store listing on every
health check. Under slow storage that can exceed the health-check timeout and
get the process restarted, all while blocking live traffic.

Fix

  • /api/health is now trivial ({"status": "ok"}). A health check must
    prove the process/event loop is alive and never touch external I/O.
  • Delegating handlers that only call synchronous helpers are now plain def,
    so the framework runs them in its threadpool (async dependencies such as the
    auth dependency still resolve on the loop). Only handlers that genuinely
    await stay async def.
  • The remaining async def handlers that do synchronous storage work offload it
    via starlette.concurrency.run_in_threadpool (the landing aggregator's
    per-scope fan-out, and the bundle-file upload PUT).

Result

The event loop stays responsive under concurrent load and slow storage; a single
in-flight fetch can no longer freeze the server or trip the health check.

Notes

  • No behavioural/API change — same responses, same auth. Purely a concurrency
    fix.
  • Follow-up worth considering: run more than one uvicorn worker so a single
    blocked worker can't monopolise a request slot.

paradoc-serve runs a single uvicorn worker (one asyncio event loop). Many
route handlers were `async def` but called SYNCHRONOUS DocStore S3 methods
(list_doc_ids, get_static_*, get_bundle_manifest, put_bundle_file, ...)
directly — each blocked the whole event loop for the duration of the S3
round-trip, so one slow request stalled every other request AND the health
probe.

Worst offender: GET /api/health returned doc_store.list_doc_ids() — a sync S3
list on EVERY 15s liveness probe. Under Garage/node slowness it blew the 5s
probe timeout and crashlooped the pod while blocking all traffic. Health is
now trivial ({"status": "ok"}) — it must never touch external I/O.

For the doc-serving endpoints: the delegating handlers that only call sync
helpers are now plain `def`, so FastAPI runs them in its threadpool (async
deps like current_user still resolve on the loop). Handlers that genuinely
await (streaming, DB, request.body) stay `async def` and offload their sync
S3 work via starlette run_in_threadpool (get_landing fan-out, bundle PUT).

Net: the event loop stays responsive under concurrent load and slow S3; a
single in-flight doc fetch no longer freezes the server or trips liveness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Krande Krande changed the title perf(serve): don't block the event loop on synchronous storage calls fix: don't block the event loop on synchronous storage calls Jul 11, 2026
@github-actions

Copy link
Copy Markdown

👋 Hi there! I have checked your PR and found the following:

PR Review:

I found no pr-related issues.

  • ✅ PR title is ok
  • ✅ Release label is ok
  • ✅ SOURCE_KEY is set as a secret
  • ✅ Skipping release

Python Review:

I found some python-related issues:

Python Linting results:

  • ✅ Isort
  • ✅ Black
  • ❌ Ruff

Python Packaging results:

@Krande Krande merged commit 4c7145e into main Jul 11, 2026
26 of 28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant