Setup: gunicorn + UvicornWorker subclass (timeout_graceful_shutdown=20), FastAPI, sse-starlette 2.4.1, uvicorn 0.51, a pure-ASGI RSS recycler that SIGTERMs its own worker when RSS is over a limit and the completed-request rate is below a busy threshold.
Symptom: LLM chat streams (SSE) closed mid-turn with no terminal event exactly when a worker recycled; logs show 'Shutting down' then 'Waiting for application shutdown' with NO 'Waiting for connections to close' line, which looks like the stream was on a sibling worker.
Measured (probe app under the real worker class): SIGTERM -> stream closed 11-12ms later; a plain 10s JSON request in the same setup DID get 'Waiting for connections to close' and drained cleanly.
Cause: sse_starlette/sse.py patches uvicorn.Server.handle_exit (AppStatus.handle_exit) at import. The signal sets a process-global event; each EventSourceResponse runs _listen_for_exit_signal in its task group and cancels the stream when it fires; a stream that starts after the flag is set ends immediately. By the time uvicorn's shutdown checks server_state.connections (after a 0.1s sleep), the SSE connection is gone, so the drain and timeout_graceful_shutdown never apply. The absence of the wait line is therefore NOT evidence the stream was elsewhere.
Second trap: a completed-requests-per-tick rate cannot see a long stream; a worker serving one SSE turn completes ~0 req/s and looks idle.
Fix: count in-flight ASGI calls in the recycler middleware (increment before await app, decrement in finally) and treat inflight > 0 like busy: defer, bounded by the max-deferral horizon, still bypassed by a hard RSS ceiling / instance memory pressure. Verified: tick logs 'recycle deferred: inflight=1' for 26s, stream delivers its terminal event, next tick recycles.