# Build a Reconnecting SSE Task Stream With Node.js

> Source: <https://dev.to/magickong/build-a-reconnecting-sse-task-stream-with-nodejs-g92>
> Published: 2026-07-14 06:16:17+00:00

A long-running AI task needs to report more than “loading.” It may be queued, editing, testing, waiting for input, retrying, or complete.

For a learning project, Server-Sent Events (SSE) are a small way to study that state flow. The server sends UTF-8 events over one HTTP response, the client remembers an event ID, and a reconnect can continue after the last accepted event.

Important scope note: [MonkeyCode](https://github.com/chaitin/MonkeyCode) uses **WebSocket**, not SSE, for the task streams I reviewed. Its [mobile task-stream client](https://github.com/chaitin/MonkeyCode/blob/c58bcd4dd4b7031f469a1271f276d22550b8f523/mobile/src/api/stream.ts) includes reconnection, queued replies, and deduplication behavior. I used those reliability concerns as inspiration for this smaller SSE exercise, not as a description of MonkeyCode's transport.

You need Node.js 20 or newer. There are no packages to install.

The complete companion project has two files:

```
sse-task-stream.mjs  # server, parser, reconnecting consumer
test-sse.mjs         # deliberate disconnect and assertions
```

An SSE record ends with a blank line. `id`

is the resume cursor, `event`

names the event type, and `data`

carries the payload.

```
res.write(
  `id: ${event.id}\n` +
  `event: task\n` +
  `data: ${JSON.stringify(event)}\n\n`
);
```

Our states are deliberately deterministic:

``` js
const events = ["queued", "running", "testing", "complete"];
```

On the first connection, the test server sends only events 1 and 2, then closes. This simulates an interrupted response without waiting for a real network failure.

`Last-Event-ID`

The client stores the greatest accepted ID. Its next request includes:

```
Last-Event-ID: 2
```

The server filters its replayable event log:

``` js
const last = Number(req.headers["last-event-id"] ?? 0);
const pending = events
  .map((state, index) => ({ id: index + 1, state }))
  .filter((event) => event.id > last);
```

This example keeps the log in memory. A production server needs durable retention, authorization, bounded history, and a snapshot rule for cursors that have expired.

Even with cursors, clients should handle replay. The consumer stores accepted events in a `Map`

keyed by ID:

``` js
const seen = new Map();
seen.set(event.id, event);
lastEventId = Math.max(lastEventId, event.id);
```

Replacing the same key makes duplicate delivery harmless for this state projection. Real effects—charging a card or merging a PR—need server-side idempotency too.

```
node test-sse.mjs
```

Expected output:

```
PASS reconnected, resumed after event 2, and converged without duplicates
```

The test asserts the exact final sequence:

```
1 queued
2 running
3 testing
4 complete
```

It also verifies that four event IDs produce four records after reconnection.

**Using SSE for two-way interactive traffic.** Browser `EventSource`

is server-to-client. Commands need separate HTTP requests, or you may prefer WebSocket when both directions are frequent.

**Treating reconnect as recovery.** A socket reopening is not enough. The client needs a cursor or a fresh authoritative snapshot.

**Using array position as permanent identity.** This demo can because the event list is fixed. Production IDs must remain stable across processes and restarts.

**Never expiring history.** Keep a retention window and define what happens when `Last-Event-ID`

is too old—usually return a snapshot cursor, not a partial story.

**Assuming exactly-once delivery.** Design for at-least-once events and idempotent projection.

After completing it, you should be able to explain the difference between connection recovery and state recovery; why an event needs an identity; how `Last-Event-ID`

supports replay; and why deduplication belongs in the consumer even when the server tries to resume precisely.

Those concepts transfer to WebSocket clients, message queues, change streams, and long-running agent interfaces. The wire format changes; the convergence problem remains.

Disclosure: I contribute to the MonkeyCode project. The MonkeyCode transport statement is based on the linked source at commit

`c58bcd4`

; this SSE project is a standalone learning implementation tested locally.
