← Blog

2026.10.15 · 4 min · javascript · browsers · backend

SSE vs WebSockets: you probably need the one-way pipe.

If only the server talks (notifications, live scores, AI tokens), Server-Sent Events give you a live stream over one ordinary HTTP response, with reconnection built into the browser. WebSockets are for when the client talks back a lot too. The format, the reconnect, and the limits people get wrong. As of 2026.10.

"Real-time" makes people reach for WebSockets by default. That buys a separate protocol, extra work on the server and the load balancer, and reconnect logic you write yourself. Most real-time features only push one way: a notification, a score, the next token of an AI reply. For those, Server-Sent Events do the job over plain HTTP, and the browser handles reconnecting for you.

Watch the 60-second version on Instagram.

Two pipes

CLIENTSERVERSSE · HTTP · TEXT/EVENT-STREAMCLIENTSERVERUPGRADEWEBSOCKET · TWO-WAY, NOT HTTPFIG. 01 / ONE WAY OVER HTTP, TWO WAYS AFTER AN UPGRADE

An SSE stream is a normal HTTP response with Content-Type: text/event-stream, always UTF-8, that simply stays open. The server keeps writing to it; the client keeps reading.

A WebSocket starts with an HTTP handshake (Sec-WebSocket-Key / Sec-WebSocket-Accept) and then stops speaking HTTP. From there it's its own two-way session.

Real-time is about latency, not direction. If the client only listens, it doesn't need a new protocol.

The whole format

The body is lines of text: data:, event:, id: and retry:, with a blank line between messages. A line starting with : is a comment, which servers often send as a keep-alive.

scores.js
// browser
const es = new EventSource("/scores");
es.onmessage = (e) => render(e.data);
response
HTTP/1.1 200 OK
Content-Type: text/event-stream
 
id: 42
data: {"home":2,"away":1}
 

es.close() stops it. That's the client.

Reconnecting is built in

404142DROPPEDRETRY: 3000LAST-EVENT-ID: 42434445FIG. 02 / DROP, WAIT, RESUME AT 43

When the connection drops, EventSource reconnects on its own. retry: sets how long it waits, in milliseconds. On the new request the browser sends a Last-Event-ID header holding the last id: it received, so the server can resume from the next event instead of starting over. (MDN doesn't spell out Last-Event-ID; the WHATWG spec does.)

There's one exception: auto-reconnect doesn't cover HTTP errors. A non-200 status or the wrong content type fails the connection for good, so a 401 or a 500 ends it. A 204 is how the server tells the client to stop.

SSE or WebSocket

SSEWebSocket
Directionserver → clientboth ways
Protocolordinary HTTP responseHTTP handshake, then its own
Reconnectbuilt in, resumes with Last-Event-IDyou write it
Datatext (UTF-8)text or binary
Good fornotifications, live scores, AI tokenschat, multiplayer

The Claude API, for one, streams its responses as SSE. EventSource works in every modern browser, about 97% of global usage.

What people get wrong

  • "Real-time means WebSockets." Only when the client talks back a lot. A one-way push needs nothing beyond HTTP.
  • "SSE is limited to 6 connections." Only on HTTP/1.1, where you get at most 6 per browser and domain, shared across tabs (marked "Won't fix" in Chrome and Firefox). On HTTP/2 the stream limit is negotiated, with a default of 100.
  • "I'll send the token in a header." EventSource takes only a URL and { withCredentials }: no method, no custom headers, always GET. Use cookies with withCredentials, or fetch() and read response.body as a stream. Don't put tokens in the URL; they end up in logs.
  • "AI chat UIs use EventSource." Usually not. They POST the prompt with fetch and read the SSE-formatted reply as a stream, because EventSource is GET-only.
  • "It just works behind any proxy." Proxies and compression can buffer the stream. Flush each event, and send : comments as keep-alives.

Two smaller ones: SSE is text only, so binary data needs a WebSocket or an encoding. And the WebSocket API has no backpressure, and an open socket can keep a page out of the bfcache.

On a client project, I'd start every server-push feature on SSE and move to WebSockets only once the client genuinely has to talk back at a high rate.

The short version

  1. SSE is one HTTP response with Content-Type: text/event-stream that stays open.
  2. new EventSource(url) reconnects by itself and resumes with Last-Event-ID; HTTP errors end it for good.
  3. EventSource is GET-only with no custom headers; use cookies or fetch streaming for auth.
  4. The 6-connection limit is HTTP/1.1 only; HTTP/2 defaults to 100 streams.
  5. Reach for WebSockets when the client talks back a lot too: chat, multiplayer.

Sources: MDN, Using server-sent events · WHATWG HTML, Server-sent events · MDN, EventSource() constructor · MDN, WebSockets API · Claude docs, Streaming messages · caniuse, Server-sent events