Skip to content
CodeBrewerz logoCodeBrewerz
Networking

WebSockets vs. Server-Sent Events: When to Use What

Both push data to the browser without polling. They are not interchangeable — one is bidirectional and stateful, the other rides plain HTTP and reconnects itself. Picking wrong costs you infrastructure, not correctness.

Published 7 min readBy Piyush Jain
  • WebSockets
  • SSE
  • Real-time
  • Networking

Real-time features almost always start with the same question: WebSockets or Server-Sent Events? Both exist to solve the same problem — polling is wasteful and slow — but they solve it with genuinely different machinery, and the difference shows up the moment you need to scale past a demo.

The short answer is that the direction of your data decides it, and almost nothing else does. The long answer is why that’s true, and what it costs you when you get it wrong.

Two mechanisms, briefly

Server-Sent Events are one HTTP response that never ends. The client makes an ordinary GET, the server replies Content-Type: text/event-stream and then simply never closes the body, writing data: lines down it as things happen. The browser’s EventSource API parses those lines into events and — this is the part people undervalue — reconnects on its own when the connection drops, telling the server where it left off. What SSE cannot do is carry anything upward. It is strictly server-to-client.

WebSockets are a bidirectional byte channel. The client sends an HTTP request with Upgrade: websocket, the server answers 101 Switching Protocols, and from that point the connection is no longer HTTP at all — it is a framed, full-duplex pipe defined by RFC 6455. Either side may send at any time, with no request to answer.

Figure 1

The axes that actually decide it

QuestionPollingSSEWebSockets
Client can send on the same channelyesevery request is upwardnoserver to client onlyyesfull duplex, any time
Your infrastructure already handles ityesit is just requestsyesit is one long GETwith workproxy config, timeouts, sticky routing
Reconnection is built inyesnothing to reconnectyesEventSource retries itselfnoyou write backoff and jitter
Replay after a gap is built inwith worknext poll gets current stateyesLast-Event-ID, if you buffernoyour own sequence numbers
Latency from event to clientnoup to one poll intervalyesimmediateyesimmediate
Survives the six-connection limityesconnections are shortwith workonly over HTTP/2yeslimit does not apply
Carries binary without re-encodingyesordinary response bodiesnotext only, so base64yesbinary frames
Works from a non-browser clientyesany HTTP clientwith worktrivial to parse, no built-in APIyeslibraries everywhere
Polling is in the table because it is often the right answer and gets dismissed unexamined. Note that the two rows where WebSockets lose outright — infrastructure compatibility and built-in reconnection — are operational rather than technical, which is why the gap only becomes visible after launch.

Read the “your infrastructure already handles it” row before the others. That is where the real cost difference lives, and it is not about the protocols being fast or slow.

Why SSE is cheaper to operate

Because to everything between your client and your server, an SSE stream is just a slow GET.

Your load balancer already knows how to route a GET. Your reverse proxy already knows how to stream a response body. Your access logs already have a line for it. Your auth middleware already reads the Cookie header on it, because it is a normal HTTP request with normal headers. Your CDN will pass it through. HTTP/2 will multiplex it alongside your other requests on the same connection.

A WebSocket asks more of every one of those. The upgrade must be forwarded rather than buffered, which means proxy configuration. Idle timeouts must be raised or your connections die at 60 seconds. Load balancers need to understand that this connection is long-lived and stateful, which usually means sticky routing. Observability tooling built around request/response has nothing useful to say about a socket that has been open for four hours.

None of that is hard. All of it is work you don’t do if a one-way stream was all you needed.

The HTTP/1.1 trap that everyone hits once

There is one specific, painful reason SSE gets abandoned by teams who then blame the protocol: browsers cap HTTP/1.1 connections at about six per origin, and an open SSE stream occupies one of those six for its entire life.

Open your app in three tabs and you have consumed three of six. Open it in six and the seventh request — any request, an image, a form post, the page reload — queues indefinitely behind streams that by design never end. The app appears to hang, and nothing in the stack reports an error, because nothing has gone wrong.

Over HTTP/2 the limit effectively disappears: streams are multiplexed onto one connection, with a default concurrency around 100 and no per-stream connection cost. So the trap has a one-line fix — serve over HTTP/2 — and the trap is only lethal on the plain-HTTP local dev server where nobody thinks to check.

WebSockets never had this problem: the connection limit does not apply to them.

What the wire actually looks like

SSE’s format is small enough to read in full, which is a real advantage when you are debugging at 2 a.m. with curl.

Figure 2

The whole SSE wire format

  • event:Dispatches as a named event, so the client uses addEventListener('price') instead of onmessage.
  • id:The browser remembers the last id and sends it as Last-Event-ID when it reconnects — replay for free, if the server keeps a buffer.
  • retry:Overrides the browser's reconnect delay. This is how you tell a stampede of clients to back off after an outage.
  • : commentA comment line is ignored by the parser but resets every idle timeout between you and the client. Send one every 15–30 seconds.
Include
Four fields, newline-delimited, one blank line per event. `id:` and `retry:` are the two that carry real weight: together they give you resumable delivery and server-controlled backoff without writing either. A WebSocket gives you a byte pipe and leaves both to you.

Two fields carry most of the value. id: is what makes reconnection nearly free — the browser stores the last id it saw and sends it back as Last-Event-ID on reconnect, so a server that keeps a short buffer of recent events can replay the gap without you writing any resumption logic. retry: lets the server dictate the reconnect delay, which is how you tell a stampede of clients to back off after an outage.

A WebSocket gives you neither and cannot: it is a byte pipe, so message ids, sequence numbers, replay-on-reconnect and backoff are all yours to design. That is the honest trade — total freedom over the message format, in exchange for having to invent the delivery semantics you get for free from SSE.

The things both of them will do to you

Whichever you choose, four problems arrive on the first day of production and are not protocol-specific:

Reconnection is not optional. Mobile networks drop connections constantly. EventSource handles this for you; with a WebSocket you write it, with exponential backoff and jitter, or you build a synchronised thundering herd that takes down your own server after every blip.

Gaps need a replay story. A client that was offline for thirty seconds missed thirty seconds of events. Either it can ask for them (a sequence number and a bounded server-side buffer) or your UI is silently wrong until the next full refresh. Decide which, on purpose.

Backpressure is real. A server that produces 1,000 events a second for a client that can absorb 100 will buffer 900 a second somewhere. Either you coalesce (send state, not deltas), sample, or drop — and if you pick none of those, the thing you have actually chosen is unbounded memory growth on the server.

Heartbeats keep the corpse from looking alive. A TCP connection through a NAT or a proxy can be dead for minutes while both endpoints still believe it is open. WebSockets have protocol-level ping/pong frames for this; SSE convention is a comment line (: keep-alive) every 15–30 seconds, which also stops intermediaries from timing out an idle stream.

So which one?

Figure 3

Pick a feature, get an answer

Answer

—

Direction of traffic
—
Why
—
What you still have to build
—
Three answers, not two. Anything one-way is SSE. Anything with a human waiting on a round trip is WebSockets. Anything in between — upward traffic that is real but rare — is SSE plus POST, which needs no sticky sessions and scales exactly like the rest of your application.

The decision reduces to one question — does the client ever need to send something on this channel? — plus one veto.

If the answer is no, use SSE. Live scores, notification feeds, build logs, an LLM streaming tokens, progress on a long job, a dashboard that updates itself: all one-way, all strictly simpler on SSE, and the streaming-token case is why SSE has had a conspicuous renaissance.

If the answer is yes and the client’s messages are frequent and latency-sensitive — chat, multiplayer, collaborative cursors, anything where a human is waiting on the round trip — use WebSockets.

If the answer is yes but the client sends rarely, do the boring thing: SSE downward and ordinary POST requests upward. This combination is underrated to the point of being forgotten. It is two well-understood HTTP mechanisms, it needs no sticky sessions, it scales exactly like the rest of your app, and it is correct for a surprising share of “we need WebSockets” features. A POST costs one round trip that a socket would have saved; if that round trip is not on a human’s critical path, you have paid nothing.

The veto: if you have a hard requirement for binary frames, or you are talking to a non-browser client, or you need to interoperate with something that already speaks WebSockets — that decides it regardless of direction. SSE is text-only (binary means base64 and a third more bytes) and EventSource is a browser API.

The short version

SSE is a GET that never finishes. WebSockets are a socket that stopped being HTTP after the handshake. The first is one-way and inherits your entire existing HTTP stack including automatic reconnection and replay-by-event-id; the second is bidirectional and asks you to build the delivery semantics, the proxy configuration and the sticky routing yourself.

Pick by direction. One-way means SSE, and serve it over HTTP/2 so the six-connection limit never bites you. Genuinely interactive means WebSockets. Occasional upward traffic means SSE plus POST, which is the answer most often skipped over for being insufficiently exciting. And whichever you pick, budget for reconnection, replay, backpressure and heartbeats on day one, because those four are the actual work and neither protocol does them for you.

Next step

Want This Built, Not Just Explained?

CodeBrewerz builds the systems these posts take apart: web, mobile, cloud and the infrastructure underneath. Tell us what you are building.

Start a conversation