WebSockets and Server-Sent Events
The Problem HTTP Wasn't Built For
HTTP is request-response. The client asks, the server answers. That's it. The server can never initiate communication. It can never say "hey, something changed" unless the client asks "has something changed?"
But modern apps need real-time updates. Chat messages. Live notifications. Stock prices. Collaborative editing. Multiplayer games. The server needs to push data to the client without waiting for a request.
Developers fought this limitation for years with hacks like long polling — opening an HTTP request and holding it open until the server has something to say. It worked, but it was ugly, resource-heavy, and didn't scale.
The web platform gave us two proper solutions: WebSockets (full duplex, bidirectional) and Server-Sent Events (server-to-client streaming). Each has a purpose. Picking the wrong one is a common mistake.
The Mental Model
HTTP is like sending letters. You write a letter (request), mail it, wait for a reply. If you want updates, you keep sending letters asking "anything new?" WebSocket is like a phone call — once connected, both sides can talk freely, anytime, no turn-taking. SSE is like a radio broadcast — the station (server) talks, you listen. You can't talk back on the same channel, but you always have HTTP for that.
WebSocket: Full Duplex Communication
WebSocket (RFC 6455) provides a persistent, bidirectional communication channel between client and server over a single TCP connection. Once the connection is established, both sides can send messages at any time without the overhead of HTTP headers.
The Upgrade Handshake
A WebSocket connection starts as an HTTP request with an Upgrade header:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
The server responds:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
After this handshake, the connection switches from HTTP to the WebSocket protocol. The TCP connection stays open, and both sides can send frames (binary or text) freely.
Client-Side API
const ws = new WebSocket('wss://example.com/chat');
ws.addEventListener('open', () => {
ws.send('Hello from client');
ws.send(JSON.stringify({ type: 'join', room: 'general' }));
});
ws.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
});
ws.addEventListener('close', (event) => {
console.log(`Closed: ${event.code} ${event.reason}`);
});
ws.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});
WebSocket Frame Structure
Once the connection is upgraded, data flows as frames, not HTTP messages:
- Text frames — UTF-8 encoded strings (most common for JSON)
- Binary frames — raw binary data (for images, audio, custom protocols)
- Ping/pong frames — keepalive mechanism to detect dead connections
- Close frame — graceful connection shutdown
Each frame has minimal overhead: 2-14 bytes of header (compared to 500-800 bytes for HTTP headers). For high-frequency messaging (game state updates, real-time telemetry), this overhead reduction is significant.
When to Use WebSocket
- Chat and messaging — both sides send messages unpredictably
- Multiplayer games — constant bidirectional state updates
- Collaborative editing — multiple cursors, real-time character-by-character sync
- Trading platforms — price updates (server to client) + order placement (client to server)
- Any scenario where both sides frequently initiate communication
Server-Sent Events: Simple Server Push
Server-Sent Events (SSE) provide a simpler, unidirectional streaming mechanism. The server sends a stream of events to the client over a regular HTTP connection. The client can't send data back over the same connection (it uses regular HTTP requests for that).
How SSE Works
The client opens an HTTP connection using the EventSource API. The server keeps the connection open and writes events as they occur:
Client:
const source = new EventSource('/api/notifications');
source.addEventListener('message', (event) => {
console.log('Data:', event.data);
});
source.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
showNotification(data.title, data.body);
});
source.addEventListener('error', () => {
console.log('Connection lost, auto-reconnecting...');
});
Server (Node.js):
app.get('/api/notifications', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const sendEvent = (type, data) => {
res.write(`event: ${type}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
sendEvent('notification', { title: 'Welcome!', body: 'Connected to stream' });
const interval = setInterval(() => {
sendEvent('heartbeat', { time: Date.now() });
}, 30000);
req.on('close', () => clearInterval(interval));
});
SSE Event Format
SSE uses a dead-simple text format:
event: notification
data: {"title": "New message", "body": "Hello!"}
id: 42
event: update
data: {"score": 100}
id: 43
Each event has:
event:— event type (optional, defaults to "message")data:— the payload (can span multiple lines)id:— event ID for reconnection (optional but important)- Blank line — terminates the event
Auto-Reconnect: SSE's Killer Feature
When the connection drops, EventSource automatically reconnects. It sends the Last-Event-ID header with the last received event ID, allowing the server to resume from where it left off.
Connection drops after event ID 42.
Browser auto-reconnects:
GET /api/notifications HTTP/1.1
Last-Event-ID: 42
Server resumes from event 43. No data lost.
This is built into the browser. You don't write reconnection logic. With WebSocket, you have to implement all of this yourself.
When to Use SSE
- Notifications — server pushes alerts, user reads them
- Live feeds — news, social media, sports scores
- Progress updates — file upload progress, build status, deployment status
- Real-time dashboards — server metrics, analytics
- AI streaming responses — LLM tokens streaming from server to client (this is how ChatGPT, Claude, and other AI chat interfaces stream responses)
- Any scenario where the server broadcasts and the client listens
WebSocket vs SSE: The Decision Framework
| Feature | WebSocket | Server-Sent Events |
|---|---|---|
| Direction | Bidirectional (both send) | Unidirectional (server to client) |
| Protocol | WebSocket (ws:// or wss://) | HTTP (regular GET request) |
| Auto-reconnect | Manual (you build it) | Built-in with Last-Event-ID |
| Binary data | Supported | Text only (Base64 for binary) |
| Connection overhead | Low (2-14 byte frame header) | Low (plain text events) |
| HTTP/2 multiplexing | No (separate TCP connection) | Yes (shares connection with other requests) |
| Proxy/firewall support | Some block WebSocket upgrades | Works everywhere (just HTTP) |
| Max connections per origin | Separate limit | Shares HTTP connection limit (6 in HTTP/1.1) |
| Best for | Chat, games, collaboration | Notifications, feeds, dashboards, AI streaming |
The Decision Tree
Does the client need to send frequent data to the server over the same channel?
- Yes (chat, games, collaboration) → WebSocket
- No (client mostly listens, uses regular HTTP for occasional sends) → SSE
Do you need binary data streaming?
- Yes → WebSocket
- No → Either works
Are you behind strict corporate firewalls that might block WebSocket upgrades?
- Yes → SSE (it's just HTTP)
- No → Either works
Do you want automatic reconnection with state recovery?
- Critical for your use case → SSE (built-in)
- You can handle it yourself → Either works
Many developers default to WebSocket for everything real-time. This is overkill for most use cases. If your server is broadcasting to clients (notifications, live data, AI streaming) and clients only occasionally send data (via regular HTTP POST), SSE is simpler, more reliable (auto-reconnect), works with HTTP/2 multiplexing, and has zero proxy issues. WebSocket is the right choice when you genuinely need high-frequency bidirectional communication.
What About Long Polling?
Before SSE and WebSocket, long polling was the standard approach. The client sends a request, and the server holds it open until it has data to send. When it responds (or times out), the client immediately sends another request.
async function longPoll() {
const response = await fetch('/api/poll');
const data = await response.json();
handleUpdate(data);
longPoll();
}
Long polling works everywhere (it's just HTTP) but has significant drawbacks:
- Resource overhead — each poll is a full HTTP request/response with headers
- Latency — there's always a gap between receiving a response and establishing the next poll
- Server load — thousands of held-open connections consume server resources
- No multiplexing — each long poll ties up an HTTP connection
Use SSE instead of long polling. SSE is literally the standardized, optimized version of long polling with auto-reconnect, event IDs, and native browser support.
HTTP/2 streams vs WebSocket for bidirectional communication
HTTP/2 supports bidirectional streaming through its multiplexed streams. In theory, you could use HTTP/2 streams for real-time communication without WebSocket. Some frameworks (like gRPC-Web) do this. However, the browser's fetch API and EventSource don't expose full duplex HTTP/2 streaming to JavaScript. WebSocket remains the only browser API that provides true bidirectional streaming with a stable, simple API. The Streams API and fetch with ReadableStream are evolving this space, but for production real-time features in 2026, WebSocket for bidirectional and SSE for unidirectional remain the pragmatic choices.
Common Mistakes
| What developers do | What they should do |
|---|---|
| Using WebSocket for everything real-time, even server-to-client only scenarios SSE is simpler, has built-in auto-reconnect, works over standard HTTP (no proxy issues), and multiplexes on HTTP/2. WebSocket adds complexity you don't need for unidirectional streaming. | Use SSE when the server broadcasts and clients mostly listen. Use WebSocket when both sides communicate frequently. |
| Not implementing ping/pong keepalive for WebSocket connections Idle TCP connections are killed by intermediate infrastructure (30-120 second timeouts are common). Without keepalive, your 'persistent' connection silently dies and the client doesn't know until it tries to send a message. | Send periodic ping frames (every 30-60 seconds) to keep the connection alive through proxies and load balancers. |
| Using long polling in new applications instead of SSE Long polling has connection overhead per poll cycle, latency gaps between polls, and no built-in reconnection. SSE is supported in all browsers, has auto-reconnect with event ID tracking, and shares the HTTP/2 connection. | SSE is the standardized, optimized replacement for long polling with better features. |
Key Takeaways
- 1WebSocket provides bidirectional communication over a persistent TCP connection. Use for chat, games, and collaborative editing where both sides send frequently.
- 2Server-Sent Events (SSE) provide unidirectional server-to-client streaming over standard HTTP. Use for notifications, dashboards, live feeds, and AI streaming.
- 3SSE has built-in auto-reconnect with Last-Event-ID for gap-free recovery. WebSocket requires manual reconnection logic.
- 4SSE works over standard HTTP and multiplexes on HTTP/2. WebSocket uses a separate TCP connection that doesn't benefit from HTTP/2.
- 5Default to SSE unless you genuinely need bidirectional communication. Most 'real-time' features are actually unidirectional.