The Fix
Upgrade to version 0.12.3 or later.
Based on closed Kludex/uvicorn issue #824 · PR/commit linked
Production note: This tends to surface only under concurrency. Reproduce with load tests and watch for lock contention/cancellation paths.
@@ -153,11 +153,14 @@ def connection_lost(self, exc):
pass
- def data_received(self, data):
+ def _unset_keepalive_if_required(self):
if self.timeout_keep_alive_task is not None:
import asyncio
async def app(scope, receive, send):
m = await receive()
if m['type'] == 'lifespan.startup':
await send({'type': 'lifespan.startup.complete'})
elif m['type'] == 'http.request':
await asyncio.sleep(.2)
await send({'type': 'http.response.start', 'status': 200, 'headers': [(b'content-length',b'5')]})
await send({'type': 'http.response.body', 'body': b'data\n', 'more_body': True})
await asyncio.sleep(.5)
await send({'type': 'http.response.body', 'body': b'', 'more_body': False})
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Option A — Upgrade to fixed release\nUpgrade to version 0.12.3 or later.\nWhen NOT to use: Do not apply if the keep-alive behavior is intentionally designed to be non-standard.\n\n
Why This Fix Works in Production
- Trigger: I encountered a race condition where uvicorn closes the connection at random. The problem seems to be incorrect management of the keep-alive timer in
- Mechanism: Incorrect management of the keep-alive timer leads to race conditions
- Why the fix works: Cancels the old keep-alive trigger before setting a new one, addressing a race condition that caused connections to close arbitrarily. (first fixed release: 0.12.3).
- If left unfixed, failures can be intermittent under concurrency (hard to reproduce; shows up as sporadic 5xx/timeouts).
Why This Breaks in Prod
- Incorrect management of the keep-alive timer leads to race conditions
- Production symptom (often without a traceback): I encountered a race condition where uvicorn closes the connection at random. The problem seems to be incorrect management of the keep-alive timer in `uvicorn/protocols/http/httptools_impl.py` (HttpToolsProtocol).
Proof / Evidence
- GitHub issue: #824
- Fix PR: https://github.com/kludex/uvicorn/pull/832
- First fixed release: 0.12.3
- Reproduced locally: No (not executed)
- Last verified: 2026-02-09
- Confidence: 0.75
- Did this fix it?: Yes (upstream fix exists)
- Own content ratio: 0.59
Discussion
High-signal excerpts from the issue thread (symptoms, repros, edge-cases).
“@itayperl The bug is reproducible using your snippet. Probably yes, the bug related to the fact, that setting new keep-alive task, we do not cancel…”
“Probably I found the solution. Once I write test cases, I'll fire a PR”
“As you can see the tests are failing without the fix https://github.com/encode/uvicorn/pull/831/checks?check_run_id=1298976600”
Failure Signature (Search String)
- I encountered a race condition where uvicorn closes the connection at random. The problem seems to be incorrect management of the keep-alive timer in
Copy-friendly signature
Failure Signature
-----------------
I encountered a race condition where uvicorn closes the connection at random. The problem seems to be incorrect management of the keep-alive timer in `uvicorn/protocols/http/httptools_impl.py` (HttpToolsProtocol).
Error Message
Signature-only (no traceback captured)
Error Message
-------------
I encountered a race condition where uvicorn closes the connection at random. The problem seems to be incorrect management of the keep-alive timer in `uvicorn/protocols/http/httptools_impl.py` (HttpToolsProtocol).
Minimal Reproduction
import asyncio
async def app(scope, receive, send):
m = await receive()
if m['type'] == 'lifespan.startup':
await send({'type': 'lifespan.startup.complete'})
elif m['type'] == 'http.request':
await asyncio.sleep(.2)
await send({'type': 'http.response.start', 'status': 200, 'headers': [(b'content-length',b'5')]})
await send({'type': 'http.response.body', 'body': b'data\n', 'more_body': True})
await asyncio.sleep(.5)
await send({'type': 'http.response.body', 'body': b'', 'more_body': False})
What Broke
Connections are closed arbitrarily, causing client requests to fail.
Why It Broke
Incorrect management of the keep-alive timer leads to race conditions
Fix Options (Details)
Option A — Upgrade to fixed release Safe default (recommended)
Upgrade to version 0.12.3 or later.
Use when you can deploy the upstream fix. It is usually lower-risk than long-lived workarounds.
Fix reference: https://github.com/kludex/uvicorn/pull/832
First fixed release: 0.12.3
Last verified: 2026-02-09. Validate in your environment.
When NOT to Use This Fix
- Do not apply if the keep-alive behavior is intentionally designed to be non-standard.
Verify Fix
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Did This Fix Work in Your Case?
Quick signal helps us prioritize which fixes to verify and improve.
Prevention
- Add a stress test that runs high-concurrency workloads and fails on thread dumps / blocked locks.
- Enable watchdog dumps in prod (faulthandler, thread dump endpoint) to capture deadlocks quickly.
- Make timeouts explicit and test them (unit + integration) to avoid silent behavior changes.
- Instrument retries (attempt count + reason) and alert on spikes to catch dependency slowdowns.
Version Compatibility Table
| Version | Status |
|---|---|
| 0.12.3 | Fixed |
Related Issues
No related fixes found.
Sources
We don’t republish the full GitHub discussion text. Use the links above for context.