An sse connection drop llm streaming failure shows up as truncated JSON, a sudden socket close, or a client timeout mid-generation. These breaks are rarely the model itself; they’re usually layers between you and the provider mishandling long-lived HTTP responses.
Step 1: Reproduce with a minimal client
Before changing infrastructure, confirm the drop is real and not a bug in your app framework. Strip everything but a raw HTTP stream reader.
curl -N -X POST https://api.example.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","stream":true,"messages":[{"role":"user","content":"Write 500 words about databases"}]}' \
-D - | cat
The -N disables curl’s buffering. If the stream dies here, the problem is upstream of your code. If it survives here but fails in your app, the bug is in your client stack.
In Python, use httpx with explicit streaming:
import httpx
url = "https://api.example.com/v1/chat/completions"
payload = {"model": "gpt-4o-mini", "stream": true, "messages": [{"role": "user", "content": "long story"}]}
headers = {"Authorization": "Bearer " + token}
with httpx.Client(timeout=httpx.Timeout(None, connect=5)) as client:
with client.stream("POST", url, json=payload, headers=headers) as r:
for line in r.iter_lines():
print(line)
Verify success: The process prints data: lines until a final data: [DONE] and exits cleanly with HTTP 200. No Connection reset or RemoteDisconnected exceptions.
Step 2: Kill proxy and gateway buffering
Most sse connection drop llm streaming incidents I’ve traced were nginx or ALB silently buffering or timing out. Nginx defaults to proxy_buffering on, which will hold chunks until the buffer fills or the response ends—defeating streaming.
location /v1/ {
proxy_pass https://upstream;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding on;
}
AWS ALB has a 60-second idle timeout by default. Streaming LLM responses can have long thinking gaps. Raise it to 1800 seconds or put a TCP-preserving proxy (like Envoy) in front.
If you run a Kubernetes ingress, check the annotation for timeout:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
Verify success: Run the curl -N command from Step 1 through the proxy and watch with curl -v. You should see Transfer-Encoding: chunked and chunks arriving every few hundred milliseconds, not one blob at the end.
Step 3: Fix client-side read timeouts
The next common cause of sse connection drop llm streaming is a client read timeout. Many SDKs default to 30s total or per-read timeouts. A model that pauses for 31s between tokens will snap the connection.
In Node.js, fetch with an AbortSignal.timeout(30000) will kill a slow stream:
// BAD
const res = await fetch(url, { method: "POST", body, signal: AbortSignal.timeout(30000) });
// GOOD
const controller = new AbortController();
// only abort on explicit user cancel, not time
const res = await fetch(url, { method: "POST", body, signal: controller.signal });
In Python, requests with stream=True still respects read_timeout. Set it to None:
import requests
r = requests.post(url, json=payload, headers=headers, stream=True, timeout=(5, None))
for line in r.iter_lines():
if line:
print(line.decode("utf-8"))
Verify success: Run a prompt that forces a long pause (e.g., “think step by step” with a slow model). The client stays connected for >2 minutes without raising ReadTimeout.
Step 4: Parse partial frames defensively
A dropped connection often leaves a half-written SSE line in your buffer. Naive split("\n") will crash on the last fragment.
def parse_sse(raw_chunks):
buf = ""
for chunk in raw_chunks:
buf += chunk.decode("utf-8")
while "\n" in buf:
line, buf = buf.split("\n", 1)
if line.startswith("data:"):
yield line[5:].strip()
# buf now holds a partial line if connection dropped
if buf.startswith("data:") and len(buf) > 5:
yield buf[5:].strip() # best-effort
Treat a missing data: [DONE] as an incomplete stream. Log the last token offset you successfully processed so you can resume.
Verify success: Artificially kill the server mid-stream (kill -9 on the proxy). Your parser should emit all complete events and log stream_incomplete=True instead of throwing JSONDecodeError.
Step 5: Use gateway fallback without trusting it blindly
An inference gateway like n4n.ai provides automatic fallback when a provider is degraded, but your client must still handle the transient sse connection drop llm streaming event. The fallback creates a new upstream connection; the old TCP socket to you is still closed. You’ll see a clean close, not an error page.
If your request included headers={"x-n4n-route": "openai->anthropic"}, the gateway may switch providers mid-flight only if it supports seamless handoff—most do not. Assume you must restart the request.
Design your prompt/state to be resumable:
def stream_with_resume(messages, last_tokens):
if last_tokens:
messages = messages + [{"role": "assistant", "content": last_tokens}]
# send request, collect tokens
Verify success: Force a provider outage in staging. Your client logs a drop, retries with prior context, and produces a complete answer without duplicating the first paragraph.
Step 6: Implement reconnection with backoff
SSE has no standard resume protocol in LLM APIs, so reconnection means re-requesting. Use exponential backoff and a max retry count.
import time, random
def resilient_stream(url, payload, headers, max_retries=4):
for attempt in range(max_retries):
try:
with httpx.Client(timeout=httpx.Timeout(None, connect=5)) as c:
with c.stream("POST", url, json=payload, headers=headers) as r:
for line in r.iter_lines():
yield line
return
except (httpx.RemoteProtocolError, httpx.ReadError) as e:
if attempt == max_retries - 1:
raise
sleep = (2 ** attempt) + random.uniform(0, 1)
time.sleep(sleep)
For browsers, use EventSource only if the API speaks pure SSE on GET; LLM endpoints require POST, so you need a custom fetch loop with the same backoff.
Verify success: Unplug your network for 5 seconds during a stream. The client pauses, then resumes and finishes the generation after reconnecting.
Step 7: Instrument drop rate in production
You cannot fix what you don’t measure. Emit a metric every time a stream terminates without [DONE].
from prometheus_client import Counter
SSE_DROPS = Counter("sse_connection_drop_llm_streaming_total", "Count of dropped streams")
def observe_stream(stream_iter):
completed = False
for line in stream_iter:
if line == "[DONE]":
completed = True
yield line
if not completed:
SSE_DROPS.inc()
Alert if drop rate exceeds 1% over a 10-minute window. Correlate with provider status pages and your proxy logs.
Verify success: A chaos test that drops 5% of connections shows the counter incrementing and the alert firing, while real traffic stays under threshold.
Production checklist
- Raw
curl -Nreproduces or rules out client bugs. - Proxy has
proxy_buffering offand idle timeout > 1h. - Client read timeout is
Noneor explicitly long. - SSE parser tolerates partial final lines.
- Reconnection backs off and resumes with prior tokens.
- Drop rate is a tracked metric, not a mystery.
Following these steps turns a flaky sse connection drop llm streaming complaint into a measured, recoverable event.