Skip to content
yutils

WebSocket vs SSE — How Real-Time Web Actually Works

Bidirectional WebSocket vs one-way Server-Sent Events — handshake details, frame formats, reconnection, heartbeats, and which one to pick for chat, notifications, or live dashboards.

~8 min read

Chat, live notifications, real-time dashboards — the server has to push data to the client. Plain HTTP's request/response loop doesn't fit. The answers are WebSocket and Server-Sent Events (SSE). How do they differ? How does bidirectional traffic ride on top of HTTP? This guide walks through the WebSocket handshake, SSE's EventSource model, the long-polling fallback, and which to pick for which job.

Why HTTP alone falls short

Default HTTP pattern:
Client → Request → Server
Server → Response → Client
                    ↓ connection closes

How to receive chat messages?
- Poll every second?
  → 99% empty responses, brutal load
- Have the server push directly?
  → How? It doesn't know the client's IP/port, and there's NAT in the way

Real-time means "server can push" + "client can send" — both directions.

The old fix — long polling

Client → GET /messages?since=0  (HTTP)
Server: holds the response open
  ...
  New message arrives!
Server → 200 OK, {messages: [...]}
       ↓ connection closes

Client → GET /messages?since=last  (immediately)
...

Pros — vanilla HTTP, plays well with proxies and firewalls. Cons:

  • New connection per message → overhead
  • HTTP headers repeat (kilobytes each)
  • Server timeouts during the hold

Used to be standard pre-2010. Today it's a fallback only.

WebSocket — bidirectional, single connection

Standardized in 2011 (RFC 6455). One HTTP handshake upgrades the connection to WebSocket; from there, bidirectional byte streams flow on the same TCP socket:

Client → GET /ws HTTP/1.1
        Host: example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Sec-WebSocket-Version: 13

Server → HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

→ Now the same TCP connection carries WebSocket frames both ways

Sec-WebSocket-Accept = SHA-1(client key + magic string). It's not a plain echo, which keeps caching proxies from accidentally completing the handshake.

WebSocket frame layout

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| op    |M| Payload len |    Extended payload length    |
|I|S|S|S| code  |A|     (7)     |             (16/64)           |
|N|V|V|V|  (4)  |S|             |                               |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+

op code:
  0x1 = text frame (UTF-8)
  0x2 = binary frame
  0x8 = close frame
  0x9 = ping frame
  0xA = pong frame

Per-frame overhead is 2-14 bytes vs HTTP's 200-2000 bytes of headers. Thousands of messages per second are routine.

Client code

const ws = new WebSocket("wss://example.com/ws");

ws.onopen = () => {
  ws.send("Hello server!");
};

ws.onmessage = (event) => {
  console.log("received:", event.data);
};

ws.onclose = (event) => {
  console.log("closed:", event.code, event.reason);
};

ws.onerror = (event) => {
  console.error("error:", event);
};

Server-Sent Events (SSE) — one-way push

W3C standard since 2009. Reuses HTTP's long-running response with a small text wrapper:

Client:
const source = new EventSource("/events");
source.onmessage = (e) => {
  console.log("server pushed:", e.data);
};

Server (Node):
res.writeHead(200, {
  "Content-Type": "text/event-stream",
  "Cache-Control": "no-cache",
  "Connection": "keep-alive",
});

// per event
res.write(`data: ${JSON.stringify(msg)}\n\n`);
// keep the connection open, never .end()

Wire format is plain text:

event: chat-message
data: {"user":"Alice","text":"hi"}
id: 42

event: notification
data: New user joined
id: 43

(blank line separates events)

SSE strengths

  • Plain HTTP — proxies and firewalls don't blink
  • Automatic reconnection — EventSource detects drops and reconnects. Last-Event-ID header lets the server resume from missed events
  • Native browser API — EventSource, no library needed

SSE weaknesses

  • One-way only — server → client. Client → server still needs separate HTTP POSTs
  • Browser connection limit — 6 per domain (HTTP/1.1). Multiple SSE tabs share the cap
  • No binary — text/event-stream only; base64 if you must

WebSocket vs SSE — at a glance

WebSocketSSE
DirectionBidirectionalServer → client only
Protocolws:// / wss://http:// / https://
BinaryX (base64 needed)
Auto reconnectRoll your own✅ built-in
Proxy / FirewallSometimes blockedPure HTTP, passes
MultiplexingOne connection + app topicsOne connection per stream
Per-message overhead2-14 bytes~50 bytes (text framing)
HTTP/2 multiplexingX (separate protocol)✅ (HTTP/2)
Server libraryDedicated (Socket.IO, ws)Vanilla HTTP, none needed

When to use which

Choose WebSocket

  • Chat (bidirectional, low latency)
  • Real-time multiplayer games
  • Collaborative editing (Google Docs-style)
  • Trading / financial tickers (lots of binary)

Choose SSE

  • Notifications (server → client only)
  • Live dashboards (stock prices, server metrics)
  • Activity feeds (Twitter, GitHub notifications)
  • Build / job progress (CI log streams)
  • Client → server can stay vanilla HTTP (form posts)

Other contenders

  • WebRTC — peer-to-peer + UDP. Low-latency voice / video / games. Less server load.
  • HTTP/2 Server Push — preload-only; not general-purpose real-time. Chrome deprecated it in 2022.
  • Long polling — legacy fallback. Last resort.

Heartbeats and reconnection

WebSocket — ping/pong

Server → ping frame (op 0x9)
Client → automatic pong frame (op 0xA)

No pong for 30+ seconds → connection is dead → reconnect

Cloudflare / nginx idle-timeout connections at 60-100s by default. 30-second heartbeats keep them alive.

SSE — automatic reconnect

Server keeps it alive with a comment line:
: heartbeat

EventSource:
- Detects connection drop
- Honors server-supplied retry: 3000 ms
- Reconnects after 3 seconds
- Sends Last-Event-ID with the last seen id
- Server resumes from missed events

Auth and security

  • WebSocket — handshake is an HTTP request (cookies + tokens included). Authenticate with the first message or a query-param token.
  • SSE — plain HTTP. Cookies, Authorization headers, and CORS apply naturally.

WebSocket gotcha — missing Origin checks enable CSWSH (Cross-Site WebSocket Hijacking). SSE inherits CORS.

Common pitfalls

1. Browser connection cap

HTTP/1.1 caps domain connections at 6. Many SSE tabs + normal fetches starve each other. HTTP/2 multiplexes around this.

2. CDN / proxy buffering

nginx and Cloudflare buffer SSE by default — events arrive in chunks instead of in real time. Set X-Accel-Buffering: no and configure the proxy.

3. Reconnect storms

After a server outage, everyone reconnects at once → thundering herd → another outage. Always exponential backoff with jitter.

4. Memory leaks

WebSocket message handlers that capture large outer-scope objects keep them alive. Call ws.close() and detach listeners explicitly.

5. Backpressure

A slow client paired with a fast publisher fills the server's buffer. Drop oldest, throttle, or coalesce.

References

  • RFC 6455 (WebSocket) — datatracker
  • MDN — Server-Sent Events — MDN
  • MDN — WebSockets API — MDN
  • WHATWG HTML — EventSource spec — WHATWG

Summary

  • Real-time options — long polling (legacy) / WebSocket (bidirectional) / SSE (server push) / WebRTC (peer-to-peer).
  • WebSocket — handshake over HTTP, then bidirectional binary or text frames. ws:// / wss://. Best for chat, games, and true two-way streams.
  • SSE — long-running HTTP response with text/event-stream. One-way, auto-reconnect, proxy-friendly. EventSource API.
  • Per-message overhead — WebSocket 2-14 bytes, SSE ~50, HTTP 1-2 KB. WebSocket is the leanest.
  • SSE's simplicity (one-way + HTTP + auto-reconnect) makes it the first pick for notifications and dashboards.
  • Heartbeats are mandatory — ping/pong for WebSocket, comment lines for SSE.
  • Watch out for CDN / proxy buffering — X-Accel-Buffering: no.
  • On reconnect: exponential backoff with jitter to avoid thundering herds.
Back to guides