# Your SSE Connection Dropped. Now What? Building Resumable Server-Sent Events in JavaScript

> Source: <https://dev.to/mwmdgmb/your-sse-connection-dropped-now-what-building-resumable-server-sent-events-in-javascript-3g95>
> Published: 2026-11-03 11:39:04+00:00

**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<OrderEvent>('/orders/stream', {
  onMessage: (data) => {
    console.log('Received:', data)
  },
})
```

…or with async iteration:

``` js
for await (const event of api.sse<OrderEvent>('/orders/stream', { signal })) {
  console.log(event.data)
}
```

With `sse-resume` enabled, tanstack-fetch:

You 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.).

Client-side resume is only half of the solution. Your server must:

Also think about event retention, expired IDs (what if the ID is older than your history?), duplicate delivery, and authorization on reconnect.

**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.

SSE resume is particularly useful for:

Whenever missing an event can leave the client in an inconsistent state, resumability deserves consideration.

SSE 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**.

You can implement these mechanisms yourself, rely on the browser's native `EventSource`, or use a fetch-based solution with automatic resume support.

If you're already using tanstack-fetch, enabling `sse-resume` is one line:

```
plugins: ['sse-resume']
```

📚 **Docs & implementation:** [https://mohamadgarmabi.github.io/tanstack-fetch/recipes/sse-resume](https://mohamadgarmabi.github.io/tanstack-fetch/recipes/sse-resume)

*tanstack-fetch is an independent open-source project, not an official TanStack package.*

💬 **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!
