{"slug": "your-sse-connection-dropped-now-what-building-resumable-server-sent-events-in", "title": "Your SSE Connection Dropped. Now What? Building Resumable Server-Sent Events in JavaScript", "summary": "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.", "body_md": "**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.\n\nReal-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.\n\nBut there's one problem that is easy to overlook:\n\n**What happens when the connection drops?**\n\nImagine you're monitoring an order:\n\n```\nOrder created       → Event 101\nPayment confirmed   → Event 102\nOrder processing    → Event 103\nConnection lost     → ❌\nOrder shipped       → Event 104\nOrder delivered     → Event 105\n```\n\nWhen the client reconnects, how does it know what happened while it was offline?\n\nLet's explore the problem, understand the SSE protocol, and implement a solution — with and without automatic SSE resume.\n\nSSE 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.\n\nA basic SSE event looks like this:\n\n```\nid: 101\nevent: order-updated\ndata: {\"status\":\"created\"}\n```\n\nThe `id` field is important. It gives the event a unique identifier that can be used to resume a stream later.\n\nHowever, receiving events over a persistent connection doesn't automatically mean your application can recover every event after a disconnection. There are two separate concerns:\n\nThe second one requires cooperation between the client *and* the server.\n\nLet's start with a typical fetch-based streaming implementation (common when you need custom headers like `Authorization`, which native `EventSource` doesn't support):\n\n``` js\nconst response = await fetch('/api/orders/stream', {\n  headers: {\n    Authorization: `Bearer ${token}`,\n  },\n})\n\nif (!response.body) {\n  throw new Error('ReadableStream is not supported')\n}\n\nconst reader = response.body.getReader()\nconst decoder = new TextDecoder()\n\nwhile (true) {\n  const { done, value } = await reader.read()\n  if (done) break\n\n  const chunk = decoder.decode(value, { stream: true })\n  console.log(chunk)\n}\n```\n\nThis consumes a stream, but it provides **no automatic reconnection and no event replay**. If the connection drops, your application needs to implement:\n\nYou *can* build all of this yourself, but the complexity grows fast.\n\n`Last-Event-ID`\nFortunately, SSE already defines a mechanism for resuming a stream: the `Last-Event-ID` header.\n\nWhen an SSE event contains an ID:\n\n```\nid: 103\ndata: {\"status\":\"processing\"}\n```\n\nthe browser's native `EventSource` remembers that ID. When the connection is interrupted and it automatically reconnects, it sends:\n\n```\nLast-Event-ID: 103\n```\n\nThe server can then replay everything after event 103:\n\n```\nClient received: 101, 102, 103\nConnection lost\nClient reconnects with Last-Event-ID: 103\n\nServer replays: 104, 105, 106\n```\n\n⚠️ This is the key distinction: **the protocol gives you the identifier and the reconnect mechanism — your server must implement event storage and replay.**\n\nHere's a small, dependency-free example that stores recent events in memory and replays them based on `Last-Event-ID`:\n\n``` python\nimport http from 'node:http'\n\nconst history = []          // [{ id, event, data }]\nconst clients = new Set()\nlet nextId = 1\nconst MAX_HISTORY = 1000\n\nfunction format({ id, event, data }) {\n  return `id: ${id}\\nevent: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`\n}\n\nfunction publish(event, data) {\n  const msg = { id: nextId++, event, data }\n  history.push(msg)\n  if (history.length > MAX_HISTORY) history.shift()\n  for (const res of clients) res.write(format(msg))\n}\n\nhttp.createServer((req, res) => {\n  if (req.url !== '/orders/stream') return res.writeHead(404).end()\n\n  res.writeHead(200, {\n    'Content-Type': 'text/event-stream',\n    'Cache-Control': 'no-cache',\n    Connection: 'keep-alive',\n  })\n  res.write('retry: 3000\\n\\n') // suggested reconnect delay\n\n  // 1) Replay what the client missed\n  const lastId = Number(req.headers['last-event-id'] ?? 0)\n  for (const msg of history) {\n    if (msg.id > lastId) res.write(format(msg))\n  }\n\n  // 2) Keep streaming new events\n  clients.add(res)\n  req.on('close', () => clients.delete(res))\n}).listen(3000)\n\n// Demo: emit an order update every 2s\nsetInterval(() => publish('order-updated', { status: 'tick', at: Date.now() }), 2000)\n```\n\nIn production you'd replace the in-memory array with Redis Streams, Kafka, a database table, or anything that gives you ordered, durable IDs.\n\nInstead of implementing reconnection and event ID tracking manually, you can use the `sse-resume` plugin in **tanstack-fetch**.\n\nConfigure your client:\n\n``` js\nimport { createFetch } from 'tanstack-fetch/sse'\n\nconst api = createFetch({\n  baseUrl: import.meta.env.VITE_API_URL,\n  plugins: ['sse-resume'],\n})\n```\n\nConsume your stream with a callback:\n\n``` js\nconst stream = api.sse<OrderEvent>('/orders/stream', {\n  onMessage: (data) => {\n    console.log('Received:', data)\n  },\n})\n```\n\n…or with async iteration:\n\n``` js\nfor await (const event of api.sse<OrderEvent>('/orders/stream', { signal })) {\n  console.log(event.data)\n}\n```\n\nWith `sse-resume` enabled, tanstack-fetch:\n\nYou don't need to build the reconnection and ID-tracking layer yourself — and unlike native `EventSource`, you keep full control over headers (auth tokens, etc.).\n\nClient-side resume is only half of the solution. Your server must:\n\nAlso think about event retention, expired IDs (what if the ID is older than your history?), duplicate delivery, and authorization on reconnect.\n\n**Important:** Resumable delivery is *not* exactly-once delivery. Use idempotent event processing or deduplication (e.g. by event ID) on the client where it matters.\n\nSSE resume is particularly useful for:\n\nWhenever missing an event can leave the client in an inconsistent state, resumability deserves consideration.\n\nSSE makes real-time communication relatively simple, but production systems need to think beyond \"open a connection and read events\". A reliable implementation should consider **reconnection, event IDs, replay, duplicate handling, and cancellation**.\n\nYou can implement these mechanisms yourself, rely on the browser's native `EventSource`, or use a fetch-based solution with automatic resume support.\n\nIf you're already using tanstack-fetch, enabling `sse-resume` is one line:\n\n```\nplugins: ['sse-resume']\n```\n\n📚 **Docs & implementation:** [https://mohamadgarmabi.github.io/tanstack-fetch/recipes/sse-resume](https://mohamadgarmabi.github.io/tanstack-fetch/recipes/sse-resume)\n\n*tanstack-fetch is an independent open-source project, not an official TanStack package.*\n\n💬 **How do you handle dropped real-time connections in your apps — SSE, WebSockets, or polling?** I'd love to hear your approach in the comments. If this was useful, a ❤️ or a 🦄 helps others find it!", "url": "https://wpnews.pro/news/your-sse-connection-dropped-now-what-building-resumable-server-sent-events-in", "canonical_source": "https://dev.to/mwmdgmb/your-sse-connection-dropped-now-what-building-resumable-server-sent-events-in-javascript-3g95", "published_at": "2026-11-03 11:39:04+00:00", "updated_at": "2026-11-03 11:53:02.642007+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Server-Sent Events", "Node.js", "JavaScript", "EventSource"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/your-sse-connection-dropped-now-what-building-resumable-server-sent-events-in", "markdown": "https://wpnews.pro/news/your-sse-connection-dropped-now-what-building-resumable-server-sent-events-in.md", "text": "https://wpnews.pro/news/your-sse-connection-dropped-now-what-building-resumable-server-sent-events-in.txt", "jsonld": "https://wpnews.pro/news/your-sse-connection-dropped-now-what-building-resumable-server-sent-events-in.jsonld"}}