n4nAI

Reconnecting dropped SSE streams with Last-Event-ID

Learn how to implement SSE reconnect with Last-Event-ID to resume dropped LLM streaming connections reliably, with runnable code examples.

n4n Team3 min read574 words

Audio narration

Coming soon — every post will get a voice note here.

Dropped connections are inevitable when you stream LLM completions over HTTP. Implementing sse reconnect last-event-id correctly lets your client resume a truncated stream without re-requesting the entire prompt, saving tokens and latency.

Step 1: Understand the SSE resume contract

The SSE spec defines a simple mechanism: each event may carry an id: field. The client remembers the last id received. If the connection closes, the client waits for the retry: period (default 3 seconds) and re-issues the GET with a Last-Event-ID HTTP header set to that id.

A server that respects this header can truncate or replay from that point. For LLM token streams, you typically assign id: to each chunk—either the token index or a UUID per chunk. The sse reconnect last-event-id flow is not magic: the server must do something sensible with the header, or the client must dedupe.

Step 2: Emit event IDs from your streaming endpoint

Below is a minimal FastAPI route that streams completion-like text with per-chunk IDs. We use a generator that yields SSE-formatted bytes.

from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio, json

app = FastAPI()

async def fake_llm_tokens(prompt: str):
    for i, tok in enumerate(["Hello", " ", "world", "!", ""]):
        await asyncio.sleep(0.1)
        yield f"id: {i}\ndata: {json.dumps({'token': tok})}\n\n"

@app.get("/stream")
async def stream(request: Request):
    return StreamingResponse(
        fake_llm_tokens(request.query_params.get("prompt", "")),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
    )

The id: {i} line is what the client will echo back as Last-Event-ID.

Step 3: Capture Last-Event-ID in a non-browser client

Browser EventSource handles this automatically. Server-side or CLI clients usually do not. Here is a Python aiohttp client that parses the stream, stores the last id, and reconnects with the header.

import aiohttp, asyncio

async def stream_with_reconnect(url, max_retries=5):
    last_id = None
    attempt = 0
    while attempt < max_retries:
        headers = {}
        if last_id is not None:
            headers["Last-Event-ID"] = str(last_id)
        try:
            async with aiohttp.ClientSession() as session:
                async with session.get(url, headers=headers) as resp:
                    async for line in resp.content:
                        line = line.decode().strip()
                        if line.startswith("id:"):
                            last_id = int(line[3:].strip())
                        elif line.startswith("data:"):
                            print("TOKEN:", line[5:].strip())
        except (aiohttp.ClientError, asyncio.TimeoutError) as e:
            print(f"drop detected: {e}, reconnecting from {last_id}")
            attempt += 1
            await asyncio.sleep(min(2 ** attempt, 30))
            continue
        break

This loop implements sse reconnect last-event-id by persisting last_id across connection failures.

Step 4: Server-side handling of Last-Event-ID

Most LLM providers cannot resume a generation mid-sequence. If you operate the endpoint, you have two options:

  1. Dedupe on client: Ignore the header server-side, but ensure IDs are monotonic. The client drops any event with id <= last_id after reconnect.
  2. Context rebuild: Use the header to know how many tokens the client already has, then re-request the completion with prior text appended.

A gateway such as n4n.ai forwards provider cache-control hints and honors routing directives, but it will not buffer an entire stream for you; your client owns resume logic.

Minimal server that skips already-sent chunks based on the header:

@app.get("/stream")
async def stream(request: Request):
    last_id = request.headers.get("Last-Event-ID")
    start = int(last_id) + 1 if last_id is not None else 0
    async def gen():
        for i, tok in enumerate(["Hello", " ", "world", "!", ""]):
            if i < start:
                continue
            await asyncio.sleep(0.1)
            yield f"id: {i}\ndata: {json.dumps({'token': tok})}\n\n"
    return StreamingResponse(gen(), media_type="text/event-stream")

Step 5: Set retry hints and backoff

The SSE spec lets the server send retry: <ms> to control client reconnect delay. If you use a custom client, implement exponential backoff with jitter.

import random
def backoff(attempt):
    return min(2 ** attempt, 30) + random.uniform(0, 1)

Send a retry directive from the server occasionally:

yield f"retry: 2000\n\n"

Step 6: Avoid duplicate side effects

If your stream triggers downstream actions (e.g., logging per token), reconnection can cause duplicates. Use the event ID as an idempotency key. Store processed IDs in a set or Redis.

processed = set()
if event_id not in processed:
    processed.add(event_id)
    handle_token(data)

Step 7: Verify the reconnection end to end

You need a way to forcibly drop the TCP connection mid-stream. Run the server locally and kill it with SIGSTOP between chunks.

Test script:

uvicorn main:app --port 8000 &
SERVER_PID=$!
sleep 2
python client.py &
CLIENT_PID=$!
sleep 0.5
kill -STOP $SERVER_PID
sleep 2
kill -CONT $SERVER_PID
wait $CLIENT_PID
kill $SERVER_PID

Success criteria: the client prints each token exactly once, and the Last-Event-ID header appears in the server logs on the second request. If you see repeated tokens, your dedupe or skip logic is broken.

Step 8: Production considerations

  • ID format: Use integer counters for simplicity; UUIDs if you shard streams.
  • Header size: Last-Event-ID is a single string; keep it small.
  • TLS: SSE over HTTPS works; ensure your client respects cert errors.
  • Gateways: When calling an OpenAI-compatible endpoint, note that the upstream may not emit id: fields. You can wrap it: assign IDs client-side by counting chunks, then apply sse reconnect last-event-id logic against your own proxy.

The sse reconnect last-event-id pattern is low-complexity insurance against flaky networks. Implement it once in your streaming client and stop losing generations to transient 503s.

Tagsssestreamingreliabilityreconnection

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All server-sent events (sse) streaming deep dive posts →