← All posts
Jul 15, 2026 · 9 min read

REST vs WebSockets vs SSE: which one do you actually need?

Most people jump straight from REST to WebSockets and skip the option in the middle. Server-Sent Events is simpler, and for a lot of features it is the right answer.

RESTWebSocketsSSEReal-time

I work on real-time systems, so I get asked this a lot. People usually frame it as REST versus WebSockets and forget there is a third option sitting between them — and that third option is often the one they actually wanted.

Step 1: The question that decides everything

Ask this about each feature:

Who needs to speak, and in which direction?

There are only three answers, and each one has a matching tool:

That is the whole decision. Everything below is detail.

Step 2: REST — the client always starts

With REST the server can never speak first. It waits to be asked, answers, and the connection closes.

Client:  GET /orders/123
Server:  { "status": "filled" }
[connection closes]

This covers most of any normal app: loading a page, submitting a form, fetching a profile. If the user triggered it, REST is right and nothing else is needed.

Step 3: SSE — the server talks, the client listens

Server-Sent Events is the one people skip. The client opens one normal HTTP connection, the server keeps it open, and pushes messages down it whenever it likes. The client cannot send anything back on that connection — it only listens.

The browser side is genuinely this small:

const events = new EventSource("/api/notifications");

events.onmessage = (event) => {
  console.log(JSON.parse(event.data));
};

And the server side, in Express:

app.get("/api/notifications", (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

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

  const timer = setInterval(() => send({ time: Date.now() }), 1000);
  req.on("close", () => clearInterval(timer));
});

That is the entire protocol — data: something\n\n over a normal HTTP response that never ends.

Why SSE is nicer than people expect

Its two real limits

  1. One direction only. To send something up, you make a normal REST call. That is usually fine — but if the client is sending constantly, it is the wrong tool.
  2. Text only. No binary frames. For JSON this does not matter.

Step 4: WebSockets — both sides talk

A WebSocket connection stays open and either side can send at any moment.

Client:  [connects, stays connected]
Server:  { "price": 42150 }
Client:  { "subscribe": "ETH-USD" }
Server:  { "price": 2280 }

Use this when the client genuinely needs to keep talking too — a chat where people type, a collaborative editor, a game, or a trading client that subscribes and unsubscribes from feeds as the user moves around.

What it costs you:

  1. You handle reconnection. Connections drop — phones change networks, laptops sleep. You write the retry, the backoff, and the catch-up.
  2. The server holds state. One open connection per user, which costs memory and makes scaling harder.
  3. Scaling across instances. If user A is on server 1 and user B on server 2, server 1 must reach B. That usually means a Redis adapter or a broker.
  4. Authorisation on every message. You authenticate once, then must keep checking this user is still allowed to receive what you are about to send.

That last one bit me. On a trading platform I worked on, the fix was splitting the socket into two namespaces: a public one streaming market data anyone could join, and an authenticated one for a user's own portfolio. Separating them meant a bug in the public feed could never leak someone's private positions.

Step 5: Side by side

                      REST        SSE          WebSocket
Direction             client→     server→      both ways
Connection            per request persistent   persistent
Protocol              HTTP        HTTP         ws:// upgrade
Auto-reconnect        n/a         yes, free    you write it
Works through proxies always      almost always sometimes blocked
Binary data           yes         no           yes
Code to write         least       little       most
Server memory         none        one per user one per user

Step 6: Do not skip plain polling

Before any of this, consider just asking again:

setInterval(() => {
  fetch("/api/notifications").then(update);
}, 30000);

If your data changes every few minutes and a 30-second delay is fine, poll. It is simpler, survives network drops for free, and needs no extra infrastructure.

Polling becomes wrong when the interval gets short. One user polling every 3 seconds is nothing. Ten thousand users polling every 3 seconds is 3,300 requests per second, nearly all returning "nothing changed". That is when a persistent connection becomes cheaper.

Step 7: What I actually reach for

  1. REST for everything the user initiates. Most of the app.
  2. Polling if updates are slow and a delay is acceptable.
  3. SSE for one-way pushes: notifications, live dashboards, progress bars, activity feeds, streaming an AI response token by token. This is the one people under-use.
  4. WebSockets only when the client must send continuously too: chat, collaborative editing, games, trading clients.

Two things worth knowing. First, you will use several of these together — that is normal, not a design failure. Second, a useful pattern for any live feature: load the current state over REST first, then apply live updates on top. A user who just reconnected sees correct data immediately instead of waiting for the next event.

Short version: client asks → REST. Server pushes and the client only listens → SSE, and it is simpler than you think. Both sides talking constantly → WebSockets, and accept the reconnection and scaling work that comes with it. If updates are rare, plain polling is still a respectable answer.

← All posts Questions? Get in touch →