Your SSE Connection Dropped. Now What? Building Resumable Server-Sent Events in JavaScript A developer has published a hands-on guide to building resumable Server-Sent Events (SSE) in JavaScript, showing that while SSE reconnects are automatic, recovering missed events requires pairing the client's Last-Event-ID header with server-side event storage and replay. The writeup includes a dependency-free Node.js server that keeps a capped in-memory history of events and replays everything after the client's last received ID, plus a fetch-based client that resumes streams automatically. TL;DR — SSE reconnects are easy; recovering the events you missed is not. The fix is id + Last-Event-ID on the client and replay on the server. Below: the problem, the protocol, a minimal Node.js replay server, and a fetch-based client that resumes automatically. Real-time applications depend on continuous data streams. Whether you're building live dashboards, order tracking systems, notification services, or AI-powered applications, Server-Sent Events SSE provide a simple way to push updates from the server to the client. But there's one problem that is easy to overlook: What happens when the connection drops? Imagine you're monitoring an order: Order created → Event 101 Payment confirmed → Event 102 Order processing → Event 103 Connection lost → ❌ Order shipped → Event 104 Order delivered → Event 105 When the client reconnects, how does it know what happened while it was offline? Let's explore the problem, understand the SSE protocol, and implement a solution — with and without automatic SSE resume. SSE is a one-way communication mechanism over HTTP. The server sends events to the client, and the connection stays open while events are being delivered. A basic SSE event looks like this: id: 101 event: order-updated data: {"status":"created"} The id field is important. It gives the event a unique identifier that can be used to resume a stream later. However, receiving events over a persistent connection doesn't automatically mean your application can recover every event after a disconnection. There are two separate concerns: The second one requires cooperation between the client and the server. Let's start with a typical fetch-based streaming implementation common when you need custom headers like Authorization , which native EventSource doesn't support : js const response = await fetch '/api/orders/stream', { headers: { Authorization: Bearer ${token} , }, } if response.body { throw new Error 'ReadableStream is not supported' } const reader = response.body.getReader const decoder = new TextDecoder while true { const { done, value } = await reader.read if done break const chunk = decoder.decode value, { stream: true } console.log chunk } This consumes a stream, but it provides no automatic reconnection and no event replay . If the connection drops, your application needs to implement: You can build all of this yourself, but the complexity grows fast. Last-Event-ID Fortunately, SSE already defines a mechanism for resuming a stream: the Last-Event-ID header. When an SSE event contains an ID: id: 103 data: {"status":"processing"} the browser's native EventSource remembers that ID. When the connection is interrupted and it automatically reconnects, it sends: Last-Event-ID: 103 The server can then replay everything after event 103: Client received: 101, 102, 103 Connection lost Client reconnects with Last-Event-ID: 103 Server replays: 104, 105, 106 ⚠️ This is the key distinction: the protocol gives you the identifier and the reconnect mechanism — your server must implement event storage and replay. Here's a small, dependency-free example that stores recent events in memory and replays them based on Last-Event-ID : python import http from 'node:http' const history = // { id, event, data } const clients = new Set let nextId = 1 const MAX HISTORY = 1000 function format { id, event, data } { return id: ${id}\nevent: ${event}\ndata: ${JSON.stringify data }\n\n } function publish event, data { const msg = { id: nextId++, event, data } history.push msg if history.length MAX HISTORY history.shift for const res of clients res.write format msg } http.createServer req, res = { if req.url == '/orders/stream' return res.writeHead 404 .end res.writeHead 200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', } res.write 'retry: 3000\n\n' // suggested reconnect delay // 1 Replay what the client missed const lastId = Number req.headers 'last-event-id' ?? 0 for const msg of history { if msg.id lastId res.write format msg } // 2 Keep streaming new events clients.add res req.on 'close', = clients.delete res } .listen 3000 // Demo: emit an order update every 2s setInterval = publish 'order-updated', { status: 'tick', at: Date.now } , 2000 In production you'd replace the in-memory array with Redis Streams, Kafka, a database table, or anything that gives you ordered, durable IDs. Instead of implementing reconnection and event ID tracking manually, you can use the sse-resume plugin in tanstack-fetch . Configure your client: js import { createFetch } from 'tanstack-fetch/sse' const api = createFetch { baseUrl: import.meta.env.VITE API URL, plugins: 'sse-resume' , } Consume your stream with a callback: js const stream = api.sse