Node.js Server-Sent Events (SSE) in 2026: The Production Guide
Server-Sent Events (SSE) used to be the quiet sibling of WebSockets — practical, simple, and almost ignored. In 2026 that changed completely. Every major AI chat product, every live dashboard, and most notification systems we audit now run on SSE. The reason is simple: when the server only needs to push, SSE is dramatically easier to operate than WebSockets and dramatically faster than polling, and it works through every CDN, load balancer, and proxy without special config.
This guide walks through everything Node.js teams need to build production-grade SSE: the wire protocol, the right framework patterns, multi-instance scaling with Redis Pub/Sub, authentication, backpressure, observability, and the gotchas that bite once you cross 10,000 concurrent connections. Code samples are runnable on Node.js 22 LTS and use only the built-in HTTP module plus the Express ecosystem you already know.
What Server-Sent Events Actually Are
SSE is a one-way streaming HTTP response. The client sends a normal GET request to a URL, the server responds with Content-Type: text/event-stream and never closes the connection — instead it writes plain-text frames separated by a blank line as new data becomes available. The browser exposes this as the EventSource API and handles reconnection, last-event-id replay, and message parsing for you.
The wire format
An SSE message looks like four optional fields followed by a blank line: event for the message type, data for the payload (any UTF-8 text, often JSON), id for the resume token, and retry to override the reconnect delay. Anything starting with a colon is a comment, useful as a keepalive ping.
event: token
data: {"text":"Hello "}
id: 18342
retry: 3000
event: token
data: {"text":"world"}
id: 18343
: keepalive ping
When SSE is the right tool
SSE wins whenever the data flow is server → client only and you can tolerate text payloads. AI token streaming, live notifications, deploy logs, stock tickers, build progress, and dashboard metrics all fit perfectly. Reach for WebSockets only when you genuinely need bidirectional traffic in the same channel — multiplayer games, collaborative editor cursors, voice/video signaling. For a deeper comparison of real-time stacks, see the chart below and our companion piece on building real-time apps with WebSockets.

Building a Minimal SSE Endpoint in Node.js
You can implement SSE with the raw http module — there is no framework magic required. The three things that matter are the headers, flushing, and a heartbeat that survives idle proxies.
A complete Express handler
import express from 'express';
const app = express();
// EventSource clients keep the connection open; do NOT use res.json or res.end
app.get('/events', (req, res) => {
// 1. SSE headers — Cache-Control and X-Accel-Buffering are critical behind proxies
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' // disable Nginx buffering
});
res.flushHeaders();
// 2. Initial hello + retry hint
res.write('retry: 5000\n\n');
// 3. Heartbeat every 25s — keeps idle proxies from killing the socket
const heartbeat = setInterval(() => res.write(': ping\n\n'), 25_000);
// 4. Push a domain event helper
const send = (event, payload, id) => {
res.write(`event: ${event}\n`);
if (id !== undefined) res.write(`id: ${id}\n`);
res.write(`data: ${JSON.stringify(payload)}\n\n`);
};
// 5. Subscribe to your real source — Redis pub/sub, EventEmitter, queue, etc.
const onMsg = (msg) => send('message', msg, msg.id);
bus.on('message', onMsg);
// 6. Cleanup on disconnect — leak prevention is the #1 SSE bug
req.on('close', () => {
clearInterval(heartbeat);
bus.off('message', onMsg);
});
});
app.listen(3000, () => console.log('SSE up on :3000'));
Consuming SSE on the Client
Browsers ship a battle-tested EventSource implementation that handles reconnection, exponential backoff, and Last-Event-ID replay automatically. For everything else — Node.js consumers, mobile apps, custom auth headers — fetch() with the streams API is the universal answer.
EventSource in the browser
const es = new EventSource('/events', { withCredentials: true });
es.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
console.log('id:', e.lastEventId, data);
});
es.addEventListener('error', () => {
// EventSource auto-reconnects; readyState 0=connecting, 1=open, 2=closed
if (es.readyState === EventSource.CLOSED) {
console.warn('Server closed; manual retry needed');
}
});fetch + ReadableStream for Node.js consumers
EventSource doesn't support custom headers, so anything authenticated with a Bearer token outside a cookie needs the streams API. The pattern below works in modern Node.js, Bun, Deno, and React Native.
async function streamEvents(url, token, onEvent) {
const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}`, Accept: 'text/event-stream' }
});
if (!res.ok) throw new Error(`SSE failed: ${res.status}`);
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buf = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += value;
let idx;
while ((idx = buf.indexOf('\n\n')) !== -1) {
const frame = buf.slice(0, idx);
buf = buf.slice(idx + 2);
const evt = parseFrame(frame); // returns {event, data, id}
if (evt.data) onEvent(evt);
}
}
}
Scaling SSE to Multiple Node.js Instances
A single Node.js process can comfortably hold 5,000–10,000 idle SSE connections, but real apps need horizontal scale. The blocker: when an event is produced on instance A, every connection on instance B must also receive it. The standard answer is a pub/sub bus that every instance subscribes to.
Redis Pub/Sub (the default)
Redis Pub/Sub is the simplest fan-out: each Node.js instance subscribes to a channel; producers PUBLISH; every subscriber gets a copy and forwards it to its local SSE clients. It is at-most-once and not durable, which is exactly right for ephemeral live updates.
import { createClient } from 'redis';
import { EventEmitter } from 'node:events';
export const bus = new EventEmitter();
bus.setMaxListeners(0); // SSE handlers attach per-connection
const sub = createClient({ url: process.env.REDIS_URL });
const pub = createClient({ url: process.env.REDIS_URL });
await Promise.all([sub.connect(), pub.connect()]);
await sub.subscribe('events:global', (raw) => {
bus.emit('message', JSON.parse(raw));
});
export const publish = (msg) =>
pub.publish('events:global', JSON.stringify({ id: Date.now(), ...msg }));When to graduate to Kafka or NATS
Use Kafka when you need durable replay (so a reconnecting client can ask for everything since Last-Event-ID), strict ordering per partition, or fan-out across teams. Use NATS JetStream for similar guarantees with much lower operational cost. Pure Redis Pub/Sub is fine until message rates exceed a few thousand per second per channel.
Hire Pre-Vetted Node.js Developers
Skip the months-long search. Our exclusive talent network has senior Node.js experts ready to join your team in 48 hours.
Authentication and Authorization for SSE
EventSource is constrained — it cannot send custom headers — so most teams pick one of three patterns: same-origin cookies, a one-time signed token in the query string, or a token-exchange endpoint that issues a short-lived SSE-only credential.
Same-origin cookie sessions
If your SSE endpoint is on the same origin as your app, just use your existing session cookie with `withCredentials: true`. This is the simplest production pattern and works with EventSource as-is.
Signed query-string tokens
For cross-origin clients or mobile apps, mint a short-lived JWT (60–120 seconds) bound to the user and the SSE channel, then pass it as `?token=...`. Validate with constant-time comparison and reject reuse beyond the connection lifetime. Never use long-lived tokens in URLs because they end up in proxy logs.
import jwt from 'jsonwebtoken';
app.get('/events', (req, res) => {
let claims;
try {
claims = jwt.verify(req.query.token, process.env.SSE_SECRET, {
audience: 'sse',
maxAge: '90s'
});
} catch {
return res.status(401).end();
}
res.locals.userId = claims.sub;
// ... continue with the SSE handler
});Production Gotchas Nobody Mentions
Compression breaks SSE
Express's compression middleware buffers responses to wait for the full body. SSE never finishes — meaning compressed SSE looks frozen. Disable compression for /events specifically, or check `req.headers.accept` and skip compression for `text/event-stream`.
HTTP/1.1 has a 6-connection-per-host limit
Browsers cap HTTP/1.1 to 6 concurrent requests per origin. Open 6 SSE tabs and the 7th hangs. Solve it with HTTP/2 (which multiplexes hundreds of streams over one socket) or by serving SSE from a dedicated subdomain. HTTP/2 is the right answer in 2026 — every modern Node.js stack and CDN supports it.
Backpressure on slow clients
If a client is slow to read, `res.write()` returns false. Most teams ignore this and accumulate kernel buffer until the socket is killed. Honor the return value and pause your producer (or drop low-priority events) when it's false; resume on the 'drain' event.
Observability: What to Measure
Standard request metrics lie about SSE because every connection is a multi-hour request. Track these instead: open connections (gauge), messages-per-second per channel (counter), per-client backlog bytes (histogram), and reconnect rate (counter). Tag by tenant and by channel — a single noisy tenant can dominate a server's CPU through fan-out alone.
OpenTelemetry's HTTP instrumentation will record `/events` as a span that lasts until the connection closes; that's not useful. Disable HTTP auto-instrumentation for the SSE route and emit a custom metric on connect, on each frame, and on close instead.
Hiring Engineers Who Know This Cold
Building production-grade SSE is mostly senior judgment: the protocol itself is small, but the right calls about pub/sub, auth, backpressure, and observability separate a system that scales from one that wakes you up at 3 a.m. If you're spinning up real-time features and need engineers who have done this at scale, HireNodeJS connects you with pre-vetted senior Node.js developers — typically a working candidate within 48 hours, no recruiter overhead. Common roles for this kind of work are backend developers with strong streaming and async fundamentals.
Adjacent skills our clients pair SSE work with most often:
Most production SSE stacks lean heavily on Redis for fan-out, and on Node.js performance tuning to handle thousands of concurrent connections per process. Teams running TypeScript-first codebases often want a TypeScript specialist for end-to-end type safety on the client EventSource consumers as well.
Hire Expert Node.js Developers — Ready in 48 Hours
Building real-time features is only half the battle — you need engineers who understand backpressure, pub/sub topology, and what happens when 10,000 connections all reconnect at once. HireNodeJS.com specialises exclusively in Node.js talent: every developer is pre-vetted on real-world projects, streaming APIs, event-driven architecture, and production deployments.
Unlike generalist platforms, our curated pool means you only speak with engineers who live and breathe Node.js. Most clients have their first developer working within 48 hours of getting in touch. Engagements start as short-term contracts and can convert to full-time hires with zero placement fee.
Wrap-Up: SSE Is the Pragmatic Real-Time Default
If your data flow is one-directional, SSE in 2026 is almost always the right default: it travels through every CDN, reconnects automatically, costs less than WebSockets to operate, and matches the streaming model that LLMs and live data feeds expect. Keep WebSockets for truly bidirectional traffic, polling for legacy compatibility — and put everything else on SSE.
Start with the minimal Express handler, add a heartbeat and X-Accel-Buffering, wire up Redis Pub/Sub when you scale past one instance, watch for compression and HTTP/1.1 connection limits, and instrument the four key metrics above. That's a production-grade SSE stack — and it's 200 lines of code, not a framework.
Frequently Asked Questions
When should I use Server-Sent Events instead of WebSockets in Node.js?
Use SSE whenever data flows only from server to client — AI streaming, notifications, dashboards, live feeds. Choose WebSockets only when you genuinely need bidirectional traffic in the same channel, such as multiplayer games or collaborative editor cursors. SSE is simpler to operate, works through every CDN, and reconnects automatically.
How many concurrent SSE connections can a single Node.js process handle?
A single Node.js 22 process comfortably handles 5,000–10,000 idle SSE connections on a 4 vCPU box, mostly bound by file descriptor limits and per-connection memory (~10KB). Beyond that you scale horizontally with Redis Pub/Sub fan-out across multiple instances behind a load balancer.
Do I need Redis to run SSE in production?
Only if you run more than one Node.js instance. With a single process, an in-memory EventEmitter is enough. The moment you scale to two or more instances, you need a fan-out bus so events produced on one node reach SSE clients connected to another — Redis Pub/Sub is the standard choice.
How do I authenticate Server-Sent Events when EventSource doesn't support headers?
Three production patterns: (1) same-origin session cookies with withCredentials, the simplest; (2) short-lived signed JWT in the query string for cross-origin clients; (3) the fetch + ReadableStream API in non-browser consumers, which supports custom headers. Avoid putting long-lived tokens in URLs.
Why does my SSE stream look frozen behind Nginx or a CDN?
Almost always proxy buffering. Set the response header X-Accel-Buffering: no for /events, disable Express compression on the SSE route, and ensure the upstream proxy isn't gzipping the stream. Heartbeats every 25 seconds also keep idle proxies from killing the socket.
Is SSE good for AI token streaming?
Yes — it is the dominant transport in 2026 for LLM token streaming. The protocol matches token-by-token output natively, the EventSource API handles reconnects, and CDNs route it without special config. Most production AI chat products you use are SSE under the hood.
Vivek Singh is the founder of Witarist and HireNodeJS.com — a platform connecting companies with pre-vetted Node.js developers. With years of experience scaling engineering teams, Vivek shares insights on hiring, tech talent, and building with Node.js.
Need a Node.js Engineer Who's Built Real-Time Streaming at Scale?
HireNodeJS connects you with pre-vetted senior Node.js engineers who have shipped SSE, WebSockets, and event-driven systems in production. Get a working developer within 48 hours — no recruiter fees, no lengthy screening.
