Jump to solution
Verify

The Fix

Based on closed Kludex/uvicorn issue #1618 · PR/commit linked

Production note: Most teams hit this during upgrades or environment changes. Roll out with a canary and smoke critical endpoints (health, OpenAPI/docs) before 100%.

Jump to Verify Open PR/Commit
@@ -92,7 +92,6 @@ def __init__( self.connections = server_state.connections self.tasks = server_state.tasks - self.default_headers = server_state.default_headers # Per-connection state
repro.py
import asyncio from email.utils import parsedate_to_datetime from contextlib import asynccontextmanager import httpx from uvicorn import Config, Server async def test_default_date_header_different(): config = Config(app=app, loop="asyncio", limit_max_requests=2) async with run_server(config): async with httpx.AsyncClient() as client: response1 = await client.get("http://127.0.0.1:8000") await asyncio.sleep(2) response2 = await client.get("http://127.0.0.1:8000") response1_date = parsedate_to_datetime(response1.headers["date"]) response2_date = parsedate_to_datetime(response2.headers["date"]) assert(response2_date > response1_date) async def app(scope, receive, send): assert scope["type"] == "http" await send({"type": "http.response.start", "status": 200, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) @asynccontextmanager async def run_server(config: Config): server = Server(config=config) cancel_handle = asyncio.ensure_future(server.serve()) await asyncio.sleep(0.1) try: yield server finally: await server.shutdown() cancel_handle.cancel() asyncio.run(test_date_header_different())
verify
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
fix.md

Why This Fix Works in Production

  • Trigger: "Date" header not changing between requests
  • Mechanism: The 'Date' header was not updated for each request when using a persistent connection
Production impact:
  • If left unfixed, the same config can fail only in production (env differences), causing startup failures or partial feature outages.

Why This Breaks in Prod

  • Shows up under Python 3.10.5 in real deployments (not just unit tests).
  • The 'Date' header was not updated for each request when using a persistent connection
  • Production symptom (often without a traceback): "Date" header not changing between requests

Proof / Evidence

Discussion

High-signal excerpts from the issue thread (symptoms, repros, edge-cases).

“> not sure about that, isn't that the keep-alive of 5s is for, the tests pass fine if for instance we set > > Correct.…”
@Kludex · 2022-09-11 · source
“This will be available in uvicorn 0.19.0.”
@Kludex · 2022-10-19 · source
“not sure about that, isn't that the keep-alive of 5s is for, the tests pass fine if for instance we set”
@euri10 · 2022-08-31 · source
“@euri10 that would mean the connection expires on TimeOut right? The discussion here was mainly focusing on repeated request using same connection. The discussion has…”
@iudeen · 2022-08-31 · source

Failure Signature (Search String)

  • "Date" header not changing between requests
  • I would expect that the "Date" header to change between requests, even when using an existing connection.
Copy-friendly signature
signature.txt
Failure Signature ----------------- "Date" header not changing between requests I would expect that the "Date" header to change between requests, even when using an existing connection.

Error Message

Signature-only (no traceback captured)
error.txt
Error Message ------------- "Date" header not changing between requests I would expect that the "Date" header to change between requests, even when using an existing connection.

Minimal Reproduction

repro.py
import asyncio from email.utils import parsedate_to_datetime from contextlib import asynccontextmanager import httpx from uvicorn import Config, Server async def test_default_date_header_different(): config = Config(app=app, loop="asyncio", limit_max_requests=2) async with run_server(config): async with httpx.AsyncClient() as client: response1 = await client.get("http://127.0.0.1:8000") await asyncio.sleep(2) response2 = await client.get("http://127.0.0.1:8000") response1_date = parsedate_to_datetime(response1.headers["date"]) response2_date = parsedate_to_datetime(response2.headers["date"]) assert(response2_date > response1_date) async def app(scope, receive, send): assert scope["type"] == "http" await send({"type": "http.response.start", "status": 200, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) @asynccontextmanager async def run_server(config: Config): server = Server(config=config) cancel_handle = asyncio.ensure_future(server.serve()) await asyncio.sleep(0.1) try: yield server finally: await server.shutdown() cancel_handle.cancel() asyncio.run(test_date_header_different())

Environment

  • Python: 3.10.5

What Broke

Responses were incorrectly cached due to a static 'Date' header, leading to stale data being served.

Why It Broke

The 'Date' header was not updated for each request when using a persistent connection

Fix Options (Details)

Option D — Guard side-effects with OnceOnly Guardrail for side-effects

Mitigate duplicate external side-effects under retries/timeouts/agent loops by gating the operation before calling external systems.

  • Place OnceOnly between your code/agent and real side-effects (Stripe, emails, CRM, APIs).
  • Use a stable key per side-effect (e.g., customer_id + action + idempotency_key).
  • Fail-safe: configure fail-open vs fail-closed based on blast radius and spend risk.
Show example snippet (optional)
onceonly.py
from onceonly import OnceOnly import os once = OnceOnly(api_key=os.environ["ONCEONLY_API_KEY"], fail_open=True) # Stable idempotency key per real side-effect. # Use a request id / job id / webhook delivery id / Stripe event id, etc. event_id = "evt_..." # replace key = f"stripe:webhook:{event_id}" res = once.check_lock(key=key, ttl=3600) if res.duplicate: return {"status": "already_processed"} # Safe to execute the side-effect exactly once. handle_event(event_id)

See OnceOnly SDK

When NOT to use: Do not use this to hide logic bugs or data corruption. Use it to block duplicate external side-effects and enforce tool permissions/spend caps.

Fix reference: https://github.com/kludex/uvicorn/pull/1706

Last verified: 2026-02-12. Validate in your environment.

Get updates

We publish verified fixes weekly. No spam.

Subscribe

When NOT to Use This Fix

  • This fix should not be applied if the application relies on the 'Date' header being static for caching purposes.
  • Do not use this to hide logic bugs or data corruption. Use it to block duplicate external side-effects and enforce tool permissions/spend caps.

Verify Fix

verify
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

  • 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.

Related Issues

No related fixes found.

Sources

We don’t republish the full GitHub discussion text. Use the links above for context.