cd /news/developer-tools/build-a-reconnecting-sse-task-stream… · home topics developer-tools article
[ARTICLE · art-58431] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Build a Reconnecting SSE Task Stream With Node.js

A developer built a reconnecting Server-Sent Events (SSE) task stream in Node.js to study state flow for long-running AI tasks. The implementation uses event IDs and the Last-Event-ID header to support replay and recovery after disconnection, with a test suite verifying duplicate-free convergence. The project serves as a learning exercise for concepts like idempotent consumers and cursor-based resumption, applicable to WebSocket clients and message queues.

read3 min views1 publishedJul 14, 2026

A long-running AI task needs to report more than “.” 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 uses WebSocket, not SSE, for the task streams I reviewed. Its mobile task-stream client 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:

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:

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:

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.

── more in #developer-tools 4 stories · sorted by recency
── more on @node.js 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/build-a-reconnecting…] indexed:0 read:3min 2026-07-14 ·