cd /news/developer-tools/your-sse-connection-dropped-now-what… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-163087] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=↑ positive

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.

by read5 min views1 publishedNov 3, 2026

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):

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:

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:

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:

const stream = api.sse<OrderEvent>('/orders/stream', {
  onMessage: (data) => {
    console.log('Received:', data)
  },
})

…or with async iteration:

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

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!

── more in #developer-tools 4 stories Β· sorted by recency
── more on @server-sent events 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/your-sse-connection-…] indexed:0 read:5min 2026-11-03 Β· β€”