Repository navigation
Keep the SSE endpoint's blocking work off the event loop - #9
Merged
Merged
Conversation
/v1/explain/stream is an async handler, but it ran the CPU-bound analysis and iterated the provider's synchronous stream inline. The Anthropic SDK and the Gemini client both stream over blocking HTTP, so for the whole enrichment (Gemini's timeout is 120s) the event loop was stuck: every other request, /healthz included, waited behind it. The Procfile runs one worker, so that is the whole service. The analysis now runs via run_in_threadpool and the provider stream via iterate_in_threadpool, the helpers Starlette provides for this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UigC7feaY8NkAy9oay3GDW
ethanzhoucool
marked this pull request as ready for review
November 3, 2026 17:20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
POST /v1/explain/streamis anasync defhandler, but it ran two blocking things directly on the event loop:explain(...), the CPU-bound static pass, which can take seconds on a large paste.for chunk in provider.stream(...). Both providers stream over blocking HTTP: the Anthropic SDK's syncmessages.stream, andhttpx.streamfor Gemini with a 120 s timeout.While an enrichment streamed, the event loop could not run anything else. Every other request waited until the model finished, including
/healthz, page loads and plain/v1/explain/all-levelscalls from other users. The Procfile runs a single uvicorn worker, so that is the whole service. Static-only streams were affected too, for the length of the analysis.Changes
api/main.py: the analysis runs viastarlette.concurrency.run_in_threadpool, and the provider's sync generator is consumed viaiterate_in_threadpool. These are the helpers Starlette provides for calling blocking code from async handlers. The SSE frames, their order and the error handling are unchanged.Testing
pyteston Python 3.13, 3.12 and 3.11: 148 passed, 3 skipped.ruffis clean, andmypyreports the same 26 errors as before.test_streaming_enrichment_does_not_block_other_requests. It uses a fake provider (no network, no API key) whosestream()blocks for 0.3 s before each of 3 chunks, the way a real SDK read does. The test sendsGET /healthz0.1 s into the stream, throughhttpx.ASGITransporton the same event loop.main:/healthz waited 0.91s behind the stream, so the test fails./healthzreturns right away while the stream is still running. I ran it 5 times with no flakes.Notes
I couldn't exercise a real provider here (no keys, and none should be used). The change only alters where the generator's
next()runs, not what it yields.Generated by Claude Code