Email copiado — support@tuurt.com
Cargando experiencia
node · August 26, 2026 · 6 min

Server-Sent Events vs WebSockets in Node.js: picking the right one for real-time updates

SSE handles a large share of real-time updates with a fraction of the operational overhead of a WebSocket server. We compare data flow direction, automatic reconnection, and compatibility with existing HTTP infrastructure, with Node.js examples.

By Tuurt Team

Server-Sent Events vs WebSockets in Node.js: picking the right one for real-time updates

Whenever a team asks us for "real time" on a feature, the conversation almost always jumps straight to WebSockets, as if it were the only tool on the shelf. In a good share of those cases, what's actually needed is for the server to push data to the client without the client ever sending anything back over that same channel, and for that job Server-Sent Events (SSE) gets the work done with a fraction of the operational overhead of standing up and maintaining a WebSocket server.

This piece compares the two technologies on the points that actually change an architecture decision: which direction the data flows, how reconnection works, and how each one behaves on top of existing HTTP infrastructure. It isn't an abstract performance comparison — it's the checklist we run through before picking one over the other on a Node project.

Direction of flow: the question that settles half the cases

SSE is one-way by design: the server keeps an HTTP connection open and writes events to the client as they happen. There's no return channel on that same connection — if the client needs to send something to the server, it does it with a plain HTTP request, separate from the stream. WebSockets are bidirectional from the start: once the handshake completes, either side can send messages at any time over the same connection.

That turns the first useful question into a concrete one: does the client need to send data back to the server frequently over the same channel it's receiving updates on, or does it only need to listen? A notification feed, a progress indicator for a background job, a dashboard reflecting changes made by other users, or a deployment log stream are all cases where the client is purely a consumer. A chat app, a collaborative editor, or a multiplayer game need the client to be constantly emitting too, and there the answer is almost always WebSockets.

It's worth resisting the temptation to reach for WebSockets on that first group of cases just because the infrastructure already exists elsewhere in the project. Every WebSocket connection the server keeps open is a stateful socket that has to be managed, scaled horizontally with something like a Redis adapter for cross-instance pub/sub, and reconnected by hand on the client. SSE hands off a good chunk of that work to the browser.

Reconnection: what EventSource gives you for free

The most practical day-to-day maintenance difference is reconnection. The browser's SSE client is the EventSource API, and it ships with automatic reconnection built in: if the connection drops, the browser retries after an interval the server itself can suggest via the stream's retry field.

const source = new EventSource('/api/events');

source.addEventListener('update', (event) => {
  const data = JSON.parse(event.data);
  updateUI(data);
});

source.onerror = () => {
  // EventSource is already retrying the connection on its own
  console.warn('SSE connection dropped, reconnecting');
};

On top of that, every event can carry an id, and when the browser reconnects it automatically sends a Last-Event-ID header with the last id it received. That lets the server pick the stream back up from where it left off instead of resending everything from scratch, as long as the server is set up to read that header and rebuild state from it.

// server: Express + text/event-stream
app.get('/api/events', (req, res) => {
  res.set({
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
  });
  res.flushHeaders();

  const lastId = req.headers['last-event-id'];
  const events = getEventsSince(lastId);

  const send = (event) => {
    res.write(`id: ${event.id}\n`);
    res.write(`event: update\n`);
    res.write(`data: ${JSON.stringify(event.payload)}\n\n`);
  };

  events.forEach(send);
  const subscription = bus.on('event', send);

  req.on('close', () => bus.off('event', subscription));
});

WebSockets come with none of this out of the box. The ws library hands you the raw connection; reconnection logic, exponential backoff, and resyncing state after a drop all have to be written by hand on the client, and rewritten every time the frontend switches libraries or frameworks. It's not that it's hard — it's extra code that SSE solves by default for the one-way case.

Connection limits and fitting into existing infrastructure

SSE is plain HTTP. That means it passes through corporate proxies, load balancers, and firewalls without any special configuration beyond allowing long-lived connections, it reuses existing cookies and auth headers with no extra step, and it doesn't require opening a different port or protocol on the infrastructure side. WebSockets need a protocol upgrade from HTTP, and while most modern proxies and load balancers handle that fine today, we still run into older infrastructure or restrictive corporate firewall rules where WebSocket connections fail silently or degrade, while an SSE connection just works because from the network's point of view it's one more HTTP request.

The place SSE historically lost ground was the per-domain concurrent connection limit browsers enforce under HTTP/1.1, which is low and shared across tabs on the same origin: opening several SSE connections to the same domain from different tabs could exhaust that limit and block other normal requests on the page. Under HTTP/2, connections are multiplexed over a single TCP socket and that restriction largely disappears, so if the server is running over HTTP/2 — increasingly the default on most hosting platforms — this argument against SSE loses most of its weight.

Where SSE genuinely avoids complexity

In Node specifically, setting up SSE doesn't need a separate library — it's a normal HTTP endpoint that keeps the response open and writes in text/event-stream format. There's no special handshake, no binary or alternate text protocol to manage, and the same auth middleware protecting the rest of the API protects the stream too, unchanged. Scaling horizontally still requires getting events to the right instance — typically through a pub/sub layer like Redis — but it doesn't require maintaining per-client state for a persistent bidirectional connection, just forwarding into an open HTTP response.

The cases where we recommend SSE over WebSockets without hesitation: server-pushed notifications, background task progress (file processing, report generation), CI/CD pipeline log streaming, and dashboard state updates where the client never needs to write back over that same channel. In any of these, standing up a WebSocket server adds infrastructure — stateful connection management, manual reconnection, often a pub/sub adapter — to solve a problem the browser already solves with EventSource and a handful of lines of server code.

When WebSockets are worth it

When the client needs to send data as often and as urgently as it receives it — real-time chat, collaborative cursors in a Google-Docs-style editor, multiplayer game state, remote device control — the answer is WebSockets, no detours. Trying to solve those cases with SSE plus a separate HTTP channel for the upload direction ends up rebuilding, with more moving parts, something WebSockets already solve in a single connection.

In the end, the decision doesn't come down to which technology is "more modern" or "faster" in the abstract — it comes down to a simple question about the actual shape of the traffic: if it's basically one-directional, use SSE. If it's genuinely two-directional, use WebSockets. Reaching for WebSockets by default in the first case is the most common source of complexity that didn't need to be there.

nodejs websockets sse real-time
← Back to blog