WebSocket and SSE — real-time communication
WebSocket and SSE — real-time communication
HTTP is fundamentally a one-request-then-one-response model. It is hard for the server to speak to the client first. In places like chat, notifications, and live updates, the server must be able to push data actively.
1. About real-time communication
| Event | Time |
|---|---|
| Comet (parent concept of long polling) | 2006 |
| WebSocket first draft (HyBi) | 2009 |
| WebSocket RFC 6455 | 2011 |
| EventSource (SSE) HTML5 standardization | 2011 ~ |
| HTTP/2 + Server Push (separate model) | 2015 (usage declined afterward) |
A bird's-eye view of the three candidates:
| Model | Direction | Protocol | Use |
|---|---|---|---|
| Short Polling | Client driven | Plain HTTP | Simple, cost-inefficient |
| Long Polling | Client driven, server holds | Plain HTTP | Fallback |
| SSE | Server → client one-way | HTTP (Content-Type: text/event-stream) | Notifications, live feeds |
| WebSocket | Bidirectional | ws:// · wss:// (HTTP Upgrade) | Chat, games, collaboration |
2. Polling
Short Polling — the client polls on a fixed interval to ask for new data.
setInterval(() => fetch('/messages?after=' + lastId), 3000);
Strength is simplicity. Downsides are many empty responses (cost-inefficient) and latency tied to the polling interval.
Long Polling — the server holds the response until new data arrives. After the response, the client immediately polls again.
Client → /messages?after=42 ─────────────┐
│ (server waits until a new message arrives)
Server ← 200 OK [{id:43,...}] ←──────────┘
Client → /messages?after=43 ─────────────┐ (immediately starts the next poll)
This was the standard flow before WebSocket. It still remains as a fallback (older browsers, some proxy environments).
3. SSE
Standardized via the EventSource API and the text/event-stream MIME type. The server keeps streaming data over a single open HTTP connection.
GET /events
Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
data: {"id":1,"text":"hi"}
event: ping
data: {}
id: 12
data: {"type":"update"}
The client opens it with the browser-native API.
const es = new EventSource('/events');
es.onmessage = (e) => console.log(JSON.parse(e.data));
es.addEventListener('ping', () => {});
When disconnected, EventSource auto-reconnects. The last received id is sent back as the Last-Event-ID header to help recover any missed events.
Characteristics:
- One-way (server → client).
- Works on HTTP/1.1 and HTTP/2. On HTTP/2, multiple SSE streams can share one connection.
- Text based.
- Plain HTTP, so it fits well with proxies and load balancers.
Frequently used for notifications, news feeds, and LLM token streaming.
4. WebSocket
Begins with the HTTP Upgrade header and switches to a separate protocol.
GET /chat
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <base64>
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <derived>
After that, frame-based bidirectional communication runs on the same TCP connection. Both text and binary are supported. ping/pong frames keep it alive.
Characteristics:
- Bidirectional, low latency.
- Being a separate protocol means some proxies or CDNs may not handle it (most do).
- The connection state burden falls on the user (reconnection, sessions, auth).
Frequently used for chat, games, collaborative editing (Yjs · CRDT), and real-time market data.
5. Node — ws · Socket.IO
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (sock) => {
sock.on('message', (m) => sock.send(`echo ${m}`));
});
Socket.IO layers automatic reconnection, rooms, and event emit on top of ws. The protocol differs slightly from standard WebSocket, so compatibility requires the Socket.IO client.
6. FastAPI · Spring WebFlux
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket('/ws')
async def ws(s: WebSocket):
await s.accept()
while True:
msg = await s.receive_text()
await s.send_text(f"echo {msg}")
Spring WebFlux is WebSocket on top of Reactive Streams. Backpressure feels natural. Spring MVC also covers the same place via @MessageMapping (STOMP over WebSocket).
Most frameworks build SSE by streaming chunked responses. FastAPI uses StreamingResponse, Express uses a res.write loop, Spring WebFlux uses Flux<ServerSentEvent<...>>.
7. Managed services
| Service | Model | Notes |
|---|---|---|
| Pusher (2010) | Managed WebSocket | Channel/event model. Rich SDKs. |
| Ably (2016) | Managed messaging | Globally distributed. Message guarantees. |
| Supabase Realtime | Postgres changes → WebSocket | DB change stream. |
| Firebase Realtime DB · Firestore | Real-time DB | The DB itself is a change stream. |
| Pub/Sub (Google) · SNS · EventBridge | Event backbone | More for backend-to-backend than direct client. |
| Centrifugo | OSS WebSocket server | Self-hostable managed alternative. |
If we want to skip the burden of running it ourselves (connections, scaling, reconnection), managed services help. When PostgreSQL is already in place and we need change notifications, something like Supabase Realtime fits naturally.
8. Which model to choose
- One-way, relatively low frequency, notifications/feeds — SSE. Plays well with HTTP infrastructure and recovers automatically.
- Bidirectional, low latency, chat/games/collaboration — WebSocket.
- Legacy proxies and old browsers — Long Polling fallback.
- DB change notifications — Supabase Realtime · CDC + backend → SSE/WebSocket.
9. Authentication and scaling
The standard EventSource API cannot add headers. Token authentication is delivered via cookies or query strings. WebSocket is similar — authenticate at handshake time.
Putting tokens in the query string can leave them in access logs, so cookies are often recommended.
As connection count grows, a single instance hits memory and FD limits.
Distribute across instances
→ Sticky session, or fan out messages with Redis Pub/Sub
ALB · NLB support WebSocket
→ ALB idle timeout defaults to 60s, so keep alive with pings
10. Common pitfalls
idle timeout — ALB, CloudFront, and some proxies cut connections at 60 seconds to a few minutes. Keep alive with ping/pong on both client and server.
Duplicate connections — clients with badly-written reconnection logic hold two connections at once and receive every message twice.
SSE EventSource header limits — the standard EventSource API cannot add headers. Token authentication goes via cookies or query.
Message order and duplication — WebSocket is a fundamentally reliable channel, but loss is possible after reconnection. The server must offer last-id-based recovery.
Missing backpressure — if the server sends faster than the client consumes, memory and connections drain. Throttle in token-sized chunks.
CORS / Origin verification — WebSocket has different same-origin policy effects. Verify the Origin header during the handshake.
No TLS — ws:// is plaintext. In production, always wss://. There are reports of plaintext WebSocket being severed in some environments (public Wi-Fi).
Closing thoughts
There is an impression that WebSocket is the right answer for real-time, but SSE is often the better fit. When the flow is one-way, SSE's simplicity reduces operational work. WebSocket is safer to introduce only where bidirectionality is truly needed.
Next
- typeorm-readonly
- spring-multi-module
See RFC 6455 — WebSocket Protocol · HTML Living Standard — Server-Sent Events · WebSockets API (MDN) · ws · Socket.IO · FastAPI WebSocket · Spring WebFlux WebSocket · Pusher · Ably · Supabase Realtime.