LEARNING OBJECTIVES โต
- Understand the browser's native automatic reconnection mechanism for SSE.
- Master the
id:wire format field and how it updatesevent.lastEventId. - Explain how the browser automatically injects the
Last-Event-IDHTTP header upon reconnecting. - Control client reconnection backoff intervals dynamically using the
retry: <milliseconds>field. - Design and architect server-side message replay ring buffers (e.g. Redis / In-Memory) to eliminate missed data during network blips.
๐ The Mental Model & Story (Intuitive Foundation)
Imagine reading a multi-volume physical encyclopedia book series with a friend over the phone.
Every time your friend reads a paragraph, they tell you:
"Paragraph 42: The Golden Gate Bridge was completed in 1937."
"Paragraph 43: It spans 1.7 miles across the Golden Gate strait."
CLIENT (Browser) SERVER (Stream)
| |
|<-- id: 42 | data: Paragraph 42 ------------------| (Client records lastEventId = "42")
|<-- id: 43 | data: Paragraph 43 ------------------| (Client records lastEventId = "43")
| |
[ X X X X X X X CELL PHONE CALL DROPS! X X X X X X X ]
| |
| (Browser waits 3 seconds, then calls back...) |
| |
|--- GET /stream (Last-Event-ID: 43) ------------->| (Server inspects header: "Client has 43")
|<-- id: 44 | data: Paragraph 44 ------------------| (Server immediately replays from 44!)
|<-- id: 45 | data: Paragraph 45 ------------------|
If your phone connection drops on paragraph 43, you do not start over from paragraph 1 when you call back! You simply say:
"Hey, my call dropped. My last paragraph was 43."
Your friend opens their notes, sees paragraphs 44, 45, and 46, and immediately begins reading from paragraph 44. You missed zero information, and didn't waste a second re-reading paragraphs 1 through 43.
This is the exact mechanism of SSE Event IDs and the Last-Event-ID header.
Technical Deep Dive & Specifications
The Reconnection Protocol Cycle
When an EventSource connection drops due to a network glitch, server deployment, or cellular handoff:
- The browser fires the
onerrorevent onEventSource. - The browser transitions
readyStateto0(CONNECTING). - The browser waits for the reconnection backoff duration (default: 3000ms, or whatever was last set via
retry:). - The browser issues a new HTTP
GETrequest to the original URL. - If the client received an
id:from any previous event, the browser automatically includes theLast-Event-IDHTTP header:
GET /api/stream HTTP/1.1
Host: api.example.com
Accept: text/event-stream
Last-Event-ID: 43
Cache-Control: no-cache
Setting Custom Reconnection Backoff with retry:
The server can dynamically control how aggressively or gently disconnected clients retry. This is crucial for preventing a "Thundering Herd" DDoS attack when an upstream server restarts:
retry: 15000
id: 104
data: {"message": "Server entering high load. Reconnect delay increased to 15s."}
Once received, the browser stores 15000ms as its new reconnection backoff timer for this stream.
Server-Side Message Replay Architecture
To support zero-data-loss resumption, the backend maintains a Ring Buffer (or Redis stream) of recent messages:
+-----------------------------------------------------------------------------------------------+
| SERVER-SIDE REPLAY BUFFER PIPELINE |
+-----------------------------------------------------------------------------------------------+
1. Incoming Request: GET /api/stream (Last-Event-ID: "102")
|
v
2. Check Server Ring Buffer: [100, 101, 102, 103, 104, 105]
|
v (Missed delta: 103, 104, 105)
3. Replay Missed Events Immediately:
-> id: 103 \n data: ... \n\n
-> id: 104 \n data: ... \n\n
-> id: 105 \n data: ... \n\n
|
v
4. Resume Live Stream Broadcast (106, 107...)
+-----------------------------------------------------------------------------------------------+
๐ป Interactive Code Playground
Below is a complete interactive simulation of Zero-Data-Loss SSE Reconnection. You can simulate network disconnects, observe the generation of missed messages in the server's buffer, and watch the client recover the exact delta via Last-Event-ID.
Starter Code
Line-by-Line Code Breakdown
- Lines 105โ109: The simulated server maintains a history array (
serverMessageHistory), modeling a real-world Redis Stream or in-memory ring buffer. - Lines 135โ150: The server emits an event every 1.5s with a strictly incrementing
id. If the client is online,clientLastEventIdupdates immediately. - Lines 169โ185: When network connectivity is restored, the client sends
Last-Event-ID: clientLastEventId. - Lines 187โ196: The server filters its message history for all events where
id > lastIdNumand flushes the missed delta immediately (highlighted in yellow in the client log).
Expected Browser Render Output
๐ Zero-Data-Loss SSE Auto-Reconnection
[ Restoring Network Connection... ]
Client Log Output:
[ID: 1] Transaction #1 processed
[ID: 2] Transaction #2 processed
--- CONNECTION LOST: Client is offline ---
(3 seconds pass... Server emits ID: 3, 4, 5 in background)
--- RECONNECTED: Sent Last-Event-ID: "2" ---
[ID: 3] Transaction #3 processed ๐ (Replayed from Buffer)
[ID: 4] Transaction #4 processed ๐ (Replayed from Buffer)
[ID: 5] Transaction #5 processed ๐ (Replayed from Buffer)
[ID: 6] Transaction #6 processed (Live Stream resumes)๐๏ธ Hands-On Exercise
๐ฏ The Challenge: Build a Replay Buffer Middleware
Instructions:
- Create a JavaScript class named
SSEReplayBuffer. - Implement:
add(eventData, eventType): Adds an event, increments a monotonic numeric ID, and prunes items beyondmaxSize(e.g. 50 items).getMissedEvents(lastEventId): Returns an array of missed events occurring strictly afterlastEventId. IflastEventIdisnullor empty, returns an empty array (or latest snapshot).
- Test your buffer by adding 10 items, simulating a client reconnecting with
lastEventId = '6', and verifying that items 7 through 10 are returned.
๐ Starter Code Sandbox
โ ๏ธ Common Pitfalls
- Using Random / Non-Sequential IDs: If your
id:is a random UUID (e.g.id: 9b1deb4d-...), the server cannot easily determine which messages occurred after that ID without expensive database timestamp queries. Use monotonically increasing IDs or time-sortable IDs (e.g., Snowflake or ULID). - Assuming
event.lastEventIdis Reset on Custom Events: If an event frame lacks anid:line, the client retains the last valididit received. An event without aniddoes not clear the previous ID. - Setting
retry:Too Low: Configuringretry: 100(100ms) can overwhelm your backend with thousands of simultaneous reconnection requests if your server restarts. Use a sensible minimum (e.g.,retry: 3000).
๐ก Pro Tips
- Redis Streams as the Ideal SSE Backend: Redis Streams (
XADD,XRANGE,XREAD) are a perfect architectural match for SSE. The Redis Stream ID (<timestamp>-<sequence>) maps 1:1 to the SSEid:field, allowingXREADto fetch missed messages instantly viaLast-Event-ID. - Resetting Event ID via Empty
id:: If the server needs to clear the client's cached event ID (for example, when a session resets), sendid:\n\n(an emptyidfield). Per WHATWG spec, this resets the client'slastEventIdto an empty string.
๐ Key Takeaways
- The
id:field assigns a persistent identifier to an event and sets the client'slastEventId. - On reconnection, the browser automatically sends the
Last-Event-IDHTTP header containing the last received ID. - The
retry: <ms>field dynamically adjusts the client's reconnection backoff duration. - A server-side replay ring buffer allows clients to recover missed messages seamlessly across brief network dropouts.
- Monotonically increasing or time-ordered IDs make delta reconciliation fast and lightweight.
- --