Real-Time Communication
Real time communication

When data must flow continuously between client and server, the standard HTTP request-response model introduces unnecessary overhead.…
# Real-Time Communication When data must flow continuously between client and server, the standard HTTP request-response model introduces unnecessary overhead. Real-time communication protocols -- WebSocket, Server-Sent Events (SSE), and WebRTC -- each solve different use cases with different trade-offs. ## Table of Contents 1. [Choosing the Right Approach](#choosing-the-right-approach) 2. [WebSocket Protocol](#websocket-protocol) 3. [Server-Sent Events (SSE)](#server-sent-events-sse) 4. [Long Polling](#long-polling) 5. [WebRTC Basics](#webrtc-basics) 6. [Connection Management Best Practices](#connection-management-best-practices) 7. [Scaling Real-Time Systems](#scaling-real-time-systems) --- ## Choosing the Right Approach
Overview
When data must flow continuously between client and server, the standard HTTP request-response model introduces unnecessary overhead. Real-time communication protocols -- WebSocket, Server-Sent Events (SSE), and WebRTC -- each solve different use cases with different trade-offs.
Manual install (npx, ZIP)
npx skills add wondelai/skills --skill real-time-communication -g -a claude-codeConnects with Agent Alley MCP · agentalley.io/mcp
Files included
real-time-communication.md15.9 KB
Read real-time-communication.md here
wondelai/
Show the full text529 lines
Real-Time Communication
When data must flow continuously between client and server, the standard HTTP request-response model introduces unnecessary overhead. Real-time communication protocols -- WebSocket, Server-Sent Events (SSE), and WebRTC -- each solve different use cases with different trade-offs.
Table of Contents
- Choosing the Right Approach
- WebSocket Protocol
- Server-Sent Events (SSE)
- Long Polling
- WebRTC Basics
- Connection Management Best Practices
- Scaling Real-Time Systems
Choosing the Right Approach
| Requirement | Best transport | Why |
|---|---|---|
| Bidirectional, low-latency messaging | WebSocket | Full-duplex, minimal per-message overhead |
| Server-to-client push (unidirectional) | SSE | Simpler API, auto-reconnect, works over HTTP |
| Periodic data updates | HTTP polling or stale-while-revalidate |
Simplest; no persistent connection needed |
| Peer-to-peer audio/video/data | WebRTC | Direct peer connection, media-optimized |
| Fallback for restricted networks | Long polling | Works everywhere HTTP works |
Default to the simplest option that meets requirements. SSE handles many "real-time" use cases without WebSocket's complexity. HTTP polling with stale-while-revalidate is sufficient when updates happen every few seconds or minutes.
WebSocket Protocol
WebSocket provides a persistent, full-duplex communication channel over a single TCP connection. After an HTTP upgrade handshake, client and server exchange frames with minimal overhead (~2-6 bytes per frame).
Connection lifecycle
1. Client sends HTTP upgrade request
2. Server responds with 101 Switching Protocols
3. Connection upgraded to WebSocket (persistent, full-duplex)
4. Client and server exchange frames freely
5. Either side sends a close frame to terminate
Opening handshake
The WebSocket connection begins as an HTTP request:
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Server response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
After this handshake, the connection is no longer HTTP -- it is a raw TCP connection with WebSocket framing.
Client implementation
class WebSocketClient {
constructor(url) {
this.url = url;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
this.messageQueue = [];
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectDelay = 1000; // Reset backoff
this.flushQueue();
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleMessage(data);
};
this.ws.onclose = (event) => {
if (!event.wasClean) {
this.scheduleReconnect();
}
};
this.ws.onerror = () => {
// onerror is always followed by onclose
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
this.messageQueue.push(data);
}
}
flushQueue() {
while (this.messageQueue.length > 0) {
this.send(this.messageQueue.shift());
}
}
scheduleReconnect() {
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
}
handleMessage(data) {
// Application-specific message handling
}
}
Frame types
| Frame type | Opcode | Purpose |
|---|---|---|
| Text | 0x1 | UTF-8 text data |
| Binary | 0x2 | Binary data (images, protobuf, etc.) |
| Ping | 0x9 | Heartbeat request |
| Pong | 0xA | Heartbeat response |
| Close | 0x8 | Connection termination |
Heartbeats and connection monitoring
Mobile networks and load balancers silently drop idle connections. Heartbeats detect dead connections:
// Client-side heartbeat
class HeartbeatWebSocket extends WebSocketClient {
constructor(url, heartbeatInterval = 30000) {
super(url);
this.heartbeatInterval = heartbeatInterval;
this.heartbeatTimer = null;
this.pongReceived = true;
}
connect() {
super.connect();
this.ws.onopen = () => {
this.startHeartbeat();
};
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (!this.pongReceived) {
// Server did not respond to last ping
this.ws.close();
return;
}
this.pongReceived = false;
this.ws.send(JSON.stringify({ type: 'ping' }));
}, this.heartbeatInterval);
}
handleMessage(data) {
if (data.type === 'pong') {
this.pongReceived = true;
return;
}
// Handle other messages
}
}
Server-side heartbeat timing:
- 30 seconds is a common interval for most use cases
- Mobile apps may use longer intervals (60-90s) to save battery
- Trading and gaming may use shorter intervals (5-10s) for faster dead connection detection
WebSocket and HTTP/2
An important caveat: WebSocket connections bypass HTTP/2 multiplexing. Each WebSocket connection is a separate TCP connection, not a stream on an existing HTTP/2 connection. For applications that open many WebSocket connections to the same origin, this can create connection overhead.
RFC 8441 defines WebSocket over HTTP/2, but browser support is limited.
Binary vs. text frames
For high-throughput applications, binary frames with a compact serialization format (Protocol Buffers, MessagePack, CBOR) significantly reduce message size:
// Text (JSON) - 84 bytes
ws.send(JSON.stringify({ type: 'position', x: 123.456, y: 789.012, t: 1679000000 }));
// Binary (Protocol Buffers) - ~20 bytes
const buffer = Position.encode({ x: 123.456, y: 789.012, t: 1679000000 }).finish();
ws.send(buffer);
Server-Sent Events (SSE)
SSE provides a simple, HTTP-based protocol for server-to-client push. The server holds an HTTP connection open and streams events to the client.
Key advantages over WebSocket
- Simpler: Works over standard HTTP; no upgrade handshake
- Auto-reconnect: The
EventSourceAPI reconnects automatically with configurable retry delay - Event IDs: Built-in support for resuming from the last received event
- Works through HTTP proxies: Standard HTTP, so no proxy configuration needed
- HTTP/2 compatible: SSE connections are HTTP/2 streams (multiplexed with other requests)
When SSE is sufficient
SSE handles the majority of "real-time" web use cases:
- Live notifications
- Real-time dashboards and monitoring
- Stock tickers and live scores
- Chat (with a separate HTTP POST for sending messages)
- Streaming AI responses (like ChatGPT)
- Build/deployment status updates
Server implementation
The server sends a response with Content-Type: text/event-stream:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"message": "Hello"}
event: notification
data: {"type": "alert", "text": "New message"}
id: 42
event: update
data: {"price": 142.50}
retry: 5000
Event format:
data:-- the event payload (can span multiple lines)event:-- event type (defaults to "message")id:-- event ID (sent asLast-Event-IDon reconnect)retry:-- reconnection delay in milliseconds
Client implementation
const eventSource = new EventSource('/api/stream');
// Default "message" events
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Message:', data);
};
// Named events
eventSource.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
showNotification(data);
});
// Connection management
eventSource.onerror = (event) => {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed by server');
} else {
console.log('Connection error, will auto-reconnect');
}
};
// Close when done
eventSource.close();
Resuming after disconnection
When the connection drops, the browser sends the last received event ID:
GET /api/stream HTTP/1.1
Last-Event-ID: 42
The server can use this to resume from where the client left off, preventing data loss during brief disconnections.
SSE with authentication
EventSource does not support custom headers. Workarounds:
// Option 1: Token in URL (less secure, logged in server access logs)
const eventSource = new EventSource('/api/stream?token=abc123');
// Option 2: Cookie-based authentication (preferred)
// Set an HttpOnly cookie first; EventSource sends cookies automatically
// Option 3: Use fetch() with ReadableStream for custom headers
async function streamWithHeaders(url, headers) {
const response = await fetch(url, { headers });
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// Parse SSE format manually
processSSEText(text);
}
}
Long Polling
Long polling is the fallback when WebSocket and SSE are unavailable (restrictive firewalls, legacy infrastructure).
How it works
- Client sends an HTTP request
- Server holds the request open until it has new data (or a timeout occurs)
- Server sends the response
- Client immediately sends a new request
- Repeat
async function longPoll(url, lastEventId = null) {
while (true) {
try {
const params = lastEventId ? `?since=${lastEventId}` : '';
const response = await fetch(`${url}${params}`, {
signal: AbortSignal.timeout(60000), // 60s timeout
});
if (response.ok) {
const data = await response.json();
lastEventId = data.id;
handleUpdate(data);
}
} catch (error) {
if (error.name === 'TimeoutError') {
// Normal timeout, reconnect immediately
continue;
}
// Network error, back off
await new Promise(r => setTimeout(r, 5000));
}
}
}
Long polling trade-offs
| Aspect | Long polling | WebSocket | SSE |
|---|---|---|---|
| Latency | Higher (new request each time) | Lowest | Low |
| Server resources | High (many open connections) | Medium | Medium |
| Complexity | Low | Medium | Low |
| Firewall compatibility | Highest | Lower | High |
| Bidirectional | Yes (via separate requests) | Yes (native) | No (one-way) |
WebRTC Basics
WebRTC enables peer-to-peer communication for audio, video, and arbitrary data between browsers without a relay server.
Architecture
Browser A ←→ Signaling Server ←→ Browser B
↕ ↕
└────── Direct P2P Connection ──┘
- Signaling: Peers exchange session descriptions (SDP) and ICE candidates through a signaling server (WebSocket, HTTP, or any mechanism)
- ICE: Interactive Connectivity Establishment discovers the best network path (direct, STUN, or TURN relay)
- DTLS/SRTP: Encrypted media and data channels over UDP
Data channels
For non-media real-time data (gaming, file transfer, screen sharing metadata), WebRTC data channels provide:
- Ordered or unordered delivery
- Reliable or unreliable (lossy) transport
- Low latency (UDP-based)
- Direct peer-to-peer (no server relay for media)
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
const dataChannel = peerConnection.createDataChannel('game', {
ordered: false, // Unordered for lower latency
maxRetransmits: 0, // Unreliable (no retransmission)
});
dataChannel.onopen = () => {
dataChannel.send(JSON.stringify({ type: 'move', x: 10, y: 20 }));
};
When to use WebRTC
- Video/audio calls
- Screen sharing
- Peer-to-peer file transfer
- Low-latency gaming (data channels)
- IoT device streaming
Do not use WebRTC for: Standard server-to-client push (use SSE or WebSocket), REST API alternatives, or scenarios where a server relay is needed anyway.
Connection Management Best Practices
Exponential backoff with jitter
When reconnecting after a failure, exponential backoff prevents thundering herd problems:
function getReconnectDelay(attempt, baseDelay = 1000, maxDelay = 30000) {
const exponentialDelay = baseDelay * Math.pow(2, attempt);
const cappedDelay = Math.min(exponentialDelay, maxDelay);
// Add random jitter (0-100% of delay) to prevent synchronized reconnects
const jitter = cappedDelay * Math.random();
return cappedDelay + jitter;
}
Without jitter, if a server restarts and 10,000 clients all reconnect simultaneously with the same backoff timing, the server faces a thundering herd of reconnection attempts.
Message queuing during disconnection
Queue outbound messages when disconnected and flush when reconnected:
class ResilientConnection {
constructor() {
this.queue = [];
this.connected = false;
}
send(message) {
if (this.connected) {
this.transport.send(message);
} else {
this.queue.push(message);
if (this.queue.length > 1000) {
this.queue.shift(); // Prevent unbounded growth
}
}
}
onReconnect() {
this.connected = true;
while (this.queue.length > 0) {
this.transport.send(this.queue.shift());
}
}
}
Connection lifecycle on mobile
Mobile connections face unique challenges:
- Network switches: WiFi to cellular transitions drop TCP connections
- Background tabs: Browsers throttle or suspend timers and connections in background tabs
- Battery optimization: OS may terminate background network activity
Strategies:
- Detect visibility changes with
document.visibilitychangeand reconnect when returning to foreground - Use shorter heartbeat intervals on mobile to detect dead connections faster
- Implement message ID tracking so the server can resume from the last acknowledged message
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// Tab became visible -- check connection health
if (!isConnectionAlive()) {
reconnect();
}
}
});
Scaling Real-Time Systems
Horizontal scaling with pub/sub
WebSocket connections are stateful -- a client connects to a specific server instance. To scale horizontally, use a pub/sub broker:
Client A → Server 1 ←→ Redis Pub/Sub ←→ Server 2 → Client B
Client C → Server 1 ←→ or NATS ←→ Server 3 → Client D
When Client A sends a message, Server 1 publishes it to Redis. All servers subscribed to that channel receive it and forward to their connected clients.
Connection limits
Each WebSocket connection consumes:
- A file descriptor on the server
- Memory for the connection state
- CPU for message processing
Practical limits:
- A single Node.js process can handle ~50,000-100,000 concurrent WebSocket connections (depending on message rate)
- Use connection limits and load balancing to distribute connections
- Consider connection pooling for services that broadcast to many clients
Load balancing considerations
- Sticky sessions: Required for WebSocket and long polling (the same client must reach the same server)
- Connection draining: When scaling down, drain connections gracefully before terminating a server instance
- Health checks: Exclude servers with too many connections or high latency from the load balancer pool
The choice of real-time transport should be driven by requirements, not technology preferences. SSE handles most server-push use cases with far less complexity than WebSocket. WebSocket is essential for bidirectional, low-latency communication. WebRTC is specialized for peer-to-peer media. Long polling is the universal fallback.
| 1 | # Real-Time Communication |
| 2 | |
| 3 | When data must flow continuously between client and server, the standard HTTP request-response model introduces unnecessary overhead. Real-time communication protocols -- WebSocket, Server-Sent Events (SSE), and WebRTC -- each solve different use cases with different trade-offs. |
| 4 | |
| 5 | |
| 6 | ## Table of Contents |
| 7 | [Choosing the Right Approach] |
| 8 | [WebSocket Protocol] |
| 9 | [Server-Sent Events (SSE)] |
| 10 | [Long Polling] |
| 11 | [WebRTC Basics] |
| 12 | [Connection Management Best Practices] |
| 13 | [Scaling Real-Time Systems] |
| 14 | |
| 15 | |
| 16 | |
| 17 | ## Choosing the Right Approach |
| 18 | |
| 19 | | Requirement | Best transport | Why | |
| 20 | |------------|---------------|-----| |
| 21 | | Bidirectional, low-latency messaging | WebSocket | Full-duplex, minimal per-message overhead | |
| 22 | | Server-to-client push (unidirectional) | SSE | Simpler API, auto-reconnect, works over HTTP | |
| 23 | | Periodic data updates | HTTP polling or `stale-while-revalidate` | Simplest; no persistent connection needed | |
| 24 | | Peer-to-peer audio/video/data | WebRTC | Direct peer connection, media-optimized | |
| 25 | | Fallback for restricted networks | Long polling | Works everywhere HTTP works | |
| 26 | |
| 27 | **Default to the simplest option that meets requirements.** SSE handles many "real-time" use cases without WebSocket's complexity. HTTP polling with `stale-while-revalidate` is sufficient when updates happen every few seconds or minutes. |
| 28 | |
| 29 | ## WebSocket Protocol |
| 30 | |
| 31 | WebSocket provides a persistent, full-duplex communication channel over a single TCP connection. After an HTTP upgrade handshake, client and server exchange frames with minimal overhead (~2-6 bytes per frame). |
| 32 | |
| 33 | ### Connection lifecycle |
| 34 | |
| 35 | |
| 36 | 1. Client sends HTTP upgrade request |
| 37 | 2. Server responds with 101 Switching Protocols |
| 38 | 3. Connection upgraded to WebSocket (persistent, full-duplex) |
| 39 | 4. Client and server exchange frames freely |
| 40 | 5. Either side sends a close frame to terminate |
| 41 | |
| 42 | |
| 43 | ### Opening handshake |
| 44 | |
| 45 | The WebSocket connection begins as an HTTP request: |
| 46 | |
| 47 | |
| 48 | GET /ws HTTP/1.1 |
| 49 | Host: example.com |
| 50 | Upgrade: websocket |
| 51 | Connection: Upgrade |
| 52 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== |
| 53 | Sec-WebSocket-Version: 13 |
| 54 | |
| 55 | |
| 56 | Server response: |
| 57 | |
| 58 | HTTP/1.1 101 Switching Protocols |
| 59 | Upgrade: websocket |
| 60 | Connection: Upgrade |
| 61 | Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= |
| 62 | |
| 63 | |
| 64 | After this handshake, the connection is no longer HTTP -- it is a raw TCP connection with WebSocket framing. |
| 65 | |
| 66 | ### Client implementation |
| 67 | |
| 68 | |
| 69 | class WebSocketClient { |
| 70 | constructor(url) { |
| 71 | this.url = url; |
| 72 | this.reconnectDelay = 1000; |
| 73 | this.maxReconnectDelay = 30000; |
| 74 | this.messageQueue = []; |
| 75 | this.connect(); |
| 76 | } |
| 77 | |
| 78 | connect() { |
| 79 | this.ws = new WebSocket(this.url); |
| 80 | |
| 81 | this.ws.onopen = () => { |
| 82 | console.log('Connected'); |
| 83 | this.reconnectDelay = 1000; // Reset backoff |
| 84 | this.flushQueue(); |
| 85 | }; |
| 86 | |
| 87 | this.ws.onmessage = (event) => { |
| 88 | const data = JSON.parse(event.data); |
| 89 | this.handleMessage(data); |
| 90 | }; |
| 91 | |
| 92 | this.ws.onclose = (event) => { |
| 93 | if (!event.wasClean) { |
| 94 | this.scheduleReconnect(); |
| 95 | } |
| 96 | }; |
| 97 | |
| 98 | this.ws.onerror = () => { |
| 99 | // onerror is always followed by onclose |
| 100 | }; |
| 101 | } |
| 102 | |
| 103 | send(data) { |
| 104 | if (this.ws.readyState === WebSocket.OPEN) { |
| 105 | this.ws.send(JSON.stringify(data)); |
| 106 | } else { |
| 107 | this.messageQueue.push(data); |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | flushQueue() { |
| 112 | while (this.messageQueue.length > 0) { |
| 113 | this.send(this.messageQueue.shift()); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | scheduleReconnect() { |
| 118 | setTimeout(() => this.connect(), this.reconnectDelay); |
| 119 | this.reconnectDelay = Math.min( |
| 120 | this.reconnectDelay * 2, |
| 121 | this.maxReconnectDelay |
| 122 | ); |
| 123 | } |
| 124 | |
| 125 | handleMessage(data) { |
| 126 | // Application-specific message handling |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | |
| 131 | ### Frame types |
| 132 | |
| 133 | | Frame type | Opcode | Purpose | |
| 134 | |-----------|--------|---------| |
| 135 | | Text | 0x1 | UTF-8 text data | |
| 136 | | Binary | 0x2 | Binary data (images, protobuf, etc.) | |
| 137 | | Ping | 0x9 | Heartbeat request | |
| 138 | | Pong | 0xA | Heartbeat response | |
| 139 | | Close | 0x8 | Connection termination | |
| 140 | |
| 141 | ### Heartbeats and connection monitoring |
| 142 | |
| 143 | Mobile networks and load balancers silently drop idle connections. Heartbeats detect dead connections: |
| 144 | |
| 145 | |
| 146 | // Client-side heartbeat |
| 147 | class HeartbeatWebSocket extends WebSocketClient { |
| 148 | constructor(url, heartbeatInterval = 30000) { |
| 149 | super(url); |
| 150 | this.heartbeatInterval = heartbeatInterval; |
| 151 | this.heartbeatTimer = null; |
| 152 | this.pongReceived = true; |
| 153 | } |
| 154 | |
| 155 | connect() { |
| 156 | super.connect(); |
| 157 | |
| 158 | this.ws.onopen = () => { |
| 159 | this.startHeartbeat(); |
| 160 | }; |
| 161 | } |
| 162 | |
| 163 | startHeartbeat() { |
| 164 | this.heartbeatTimer = setInterval(() => { |
| 165 | if (!this.pongReceived) { |
| 166 | // Server did not respond to last ping |
| 167 | this.ws.close(); |
| 168 | return; |
| 169 | } |
| 170 | this.pongReceived = false; |
| 171 | this.ws.send(JSON.stringify({ type: 'ping' })); |
| 172 | }, this.heartbeatInterval); |
| 173 | } |
| 174 | |
| 175 | handleMessage(data) { |
| 176 | if (data.type === 'pong') { |
| 177 | this.pongReceived = true; |
| 178 | return; |
| 179 | } |
| 180 | // Handle other messages |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | |
| 185 | **Server-side heartbeat timing:** |
| 186 | 30 seconds is a common interval for most use cases |
| 187 | Mobile apps may use longer intervals (60-90s) to save battery |
| 188 | Trading and gaming may use shorter intervals (5-10s) for faster dead connection detection |
| 189 | |
| 190 | ### WebSocket and HTTP/2 |
| 191 | |
| 192 | An important caveat: **WebSocket connections bypass HTTP/2 multiplexing.** Each WebSocket connection is a separate TCP connection, not a stream on an existing HTTP/2 connection. For applications that open many WebSocket connections to the same origin, this can create connection overhead. |
| 193 | |
| 194 | RFC 8441 defines WebSocket over HTTP/2, but browser support is limited. |
| 195 | |
| 196 | ### Binary vs. text frames |
| 197 | |
| 198 | For high-throughput applications, binary frames with a compact serialization format (Protocol Buffers, MessagePack, CBOR) significantly reduce message size: |
| 199 | |
| 200 | |
| 201 | // Text (JSON) - 84 bytes |
| 202 | ws.send(JSON.stringify({ type: 'position', x: 123.456, y: 789.012, t: 1679000000 })); |
| 203 | |
| 204 | // Binary (Protocol Buffers) - ~20 bytes |
| 205 | const buffer = Position.encode({ x: 123.456, y: 789.012, t: 1679000000 }).finish(); |
| 206 | ws.send(buffer); |
| 207 | |
| 208 | |
| 209 | ## Server-Sent Events (SSE) |
| 210 | |
| 211 | SSE provides a simple, HTTP-based protocol for server-to-client push. The server holds an HTTP connection open and streams events to the client. |
| 212 | |
| 213 | ### Key advantages over WebSocket |
| 214 | |
| 215 | **Simpler:** Works over standard HTTP; no upgrade handshake |
| 216 | **Auto-reconnect:** The `EventSource` API reconnects automatically with configurable retry delay |
| 217 | **Event IDs:** Built-in support for resuming from the last received event |
| 218 | **Works through HTTP proxies:** Standard HTTP, so no proxy configuration needed |
| 219 | **HTTP/2 compatible:** SSE connections are HTTP/2 streams (multiplexed with other requests) |
| 220 | |
| 221 | ### When SSE is sufficient |
| 222 | |
| 223 | SSE handles the majority of "real-time" web use cases: |
| 224 | Live notifications |
| 225 | Real-time dashboards and monitoring |
| 226 | Stock tickers and live scores |
| 227 | Chat (with a separate HTTP POST for sending messages) |
| 228 | Streaming AI responses (like ChatGPT) |
| 229 | Build/deployment status updates |
| 230 | |
| 231 | ### Server implementation |
| 232 | |
| 233 | The server sends a response with `Content-Type: text/event-stream`: |
| 234 | |
| 235 | |
| 236 | HTTP/1.1 200 OK |
| 237 | Content-Type: text/event-stream |
| 238 | Cache-Control: no-cache |
| 239 | Connection: keep-alive |
| 240 | |
| 241 | data: {"message": "Hello"} |
| 242 | |
| 243 | event: notification |
| 244 | data: {"type": "alert", "text": "New message"} |
| 245 | |
| 246 | id: 42 |
| 247 | event: update |
| 248 | data: {"price": 142.50} |
| 249 | |
| 250 | retry: 5000 |
| 251 | |
| 252 | |
| 253 | |
| 254 | Event format: |
| 255 | `data:` -- the event payload (can span multiple lines) |
| 256 | `event:` -- event type (defaults to "message") |
| 257 | `id:` -- event ID (sent as `Last-Event-ID` on reconnect) |
| 258 | `retry:` -- reconnection delay in milliseconds |
| 259 | |
| 260 | ### Client implementation |
| 261 | |
| 262 | |
| 263 | const eventSource = new EventSource('/api/stream'); |
| 264 | |
| 265 | // Default "message" events |
| 266 | eventSource.onmessage = (event) => { |
| 267 | const data = JSON.parse(event.data); |
| 268 | console.log('Message:', data); |
| 269 | }; |
| 270 | |
| 271 | // Named events |
| 272 | eventSource.addEventListener('notification', (event) => { |
| 273 | const data = JSON.parse(event.data); |
| 274 | showNotification(data); |
| 275 | }); |
| 276 | |
| 277 | // Connection management |
| 278 | eventSource.onerror = (event) => { |
| 279 | if (eventSource.readyState === EventSource.CLOSED) { |
| 280 | console.log('Connection closed by server'); |
| 281 | } else { |
| 282 | console.log('Connection error, will auto-reconnect'); |
| 283 | } |
| 284 | }; |
| 285 | |
| 286 | // Close when done |
| 287 | eventSource.close(); |
| 288 | |
| 289 | |
| 290 | ### Resuming after disconnection |
| 291 | |
| 292 | When the connection drops, the browser sends the last received event ID: |
| 293 | |
| 294 | |
| 295 | GET /api/stream HTTP/1.1 |
| 296 | Last-Event-ID: 42 |
| 297 | |
| 298 | |
| 299 | The server can use this to resume from where the client left off, preventing data loss during brief disconnections. |
| 300 | |
| 301 | ### SSE with authentication |
| 302 | |
| 303 | `EventSource` does not support custom headers. Workarounds: |
| 304 | |
| 305 | |
| 306 | // Option 1: Token in URL (less secure, logged in server access logs) |
| 307 | const eventSource = new EventSource('/api/stream?token=abc123'); |
| 308 | |
| 309 | // Option 2: Cookie-based authentication (preferred) |
| 310 | // Set an HttpOnly cookie first; EventSource sends cookies automatically |
| 311 | |
| 312 | // Option 3: Use fetch() with ReadableStream for custom headers |
| 313 | async function streamWithHeaders(url, headers) { |
| 314 | const response = await fetch(url, { headers }); |
| 315 | const reader = response.body.getReader(); |
| 316 | const decoder = new TextDecoder(); |
| 317 | |
| 318 | while (true) { |
| 319 | const { done, value } = await reader.read(); |
| 320 | if (done) break; |
| 321 | const text = decoder.decode(value); |
| 322 | // Parse SSE format manually |
| 323 | processSSEText(text); |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | |
| 328 | ## Long Polling |
| 329 | |
| 330 | Long polling is the fallback when WebSocket and SSE are unavailable (restrictive firewalls, legacy infrastructure). |
| 331 | |
| 332 | ### How it works |
| 333 | |
| 334 | Client sends an HTTP request |
| 335 | Server holds the request open until it has new data (or a timeout occurs) |
| 336 | Server sends the response |
| 337 | Client immediately sends a new request |
| 338 | Repeat |
| 339 | |
| 340 | |
| 341 | async function longPoll(url, lastEventId = null) { |
| 342 | while (true) { |
| 343 | try { |
| 344 | const params = lastEventId ? `?since=${lastEventId}` : ''; |
| 345 | const response = await fetch(`${url}${params}`, { |
| 346 | signal: AbortSignal.timeout(60000), // 60s timeout |
| 347 | }); |
| 348 | |
| 349 | if (response.ok) { |
| 350 | const data = await response.json(); |
| 351 | lastEventId = data.id; |
| 352 | handleUpdate(data); |
| 353 | } |
| 354 | } catch (error) { |
| 355 | if (error.name === 'TimeoutError') { |
| 356 | // Normal timeout, reconnect immediately |
| 357 | continue; |
| 358 | } |
| 359 | // Network error, back off |
| 360 | await new Promise(r => setTimeout(r, 5000)); |
| 361 | } |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | |
| 366 | ### Long polling trade-offs |
| 367 | |
| 368 | | Aspect | Long polling | WebSocket | SSE | |
| 369 | |--------|-------------|-----------|-----| |
| 370 | | Latency | Higher (new request each time) | Lowest | Low | |
| 371 | | Server resources | High (many open connections) | Medium | Medium | |
| 372 | | Complexity | Low | Medium | Low | |
| 373 | | Firewall compatibility | Highest | Lower | High | |
| 374 | | Bidirectional | Yes (via separate requests) | Yes (native) | No (one-way) | |
| 375 | |
| 376 | ## WebRTC Basics |
| 377 | |
| 378 | WebRTC enables peer-to-peer communication for audio, video, and arbitrary data between browsers without a relay server. |
| 379 | |
| 380 | ### Architecture |
| 381 | |
| 382 | |
| 383 | Browser A ←→ Signaling Server ←→ Browser B |
| 384 | ↕ ↕ |
| 385 | └────── Direct P2P Connection ──┘ |
| 386 | |
| 387 | |
| 388 | **Signaling:** Peers exchange session descriptions (SDP) and ICE candidates through a signaling server (WebSocket, HTTP, or any mechanism) |
| 389 | **ICE:** Interactive Connectivity Establishment discovers the best network path (direct, STUN, or TURN relay) |
| 390 | **DTLS/SRTP:** Encrypted media and data channels over UDP |
| 391 | |
| 392 | ### Data channels |
| 393 | |
| 394 | For non-media real-time data (gaming, file transfer, screen sharing metadata), WebRTC data channels provide: |
| 395 | Ordered or unordered delivery |
| 396 | Reliable or unreliable (lossy) transport |
| 397 | Low latency (UDP-based) |
| 398 | Direct peer-to-peer (no server relay for media) |
| 399 | |
| 400 | |
| 401 | const peerConnection = new RTCPeerConnection({ |
| 402 | iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] |
| 403 | }); |
| 404 | |
| 405 | const dataChannel = peerConnection.createDataChannel('game', { |
| 406 | ordered: false, // Unordered for lower latency |
| 407 | maxRetransmits: 0, // Unreliable (no retransmission) |
| 408 | }); |
| 409 | |
| 410 | dataChannel.onopen = () => { |
| 411 | dataChannel.send(JSON.stringify({ type: 'move', x: 10, y: 20 })); |
| 412 | }; |
| 413 | |
| 414 | |
| 415 | ### When to use WebRTC |
| 416 | |
| 417 | Video/audio calls |
| 418 | Screen sharing |
| 419 | Peer-to-peer file transfer |
| 420 | Low-latency gaming (data channels) |
| 421 | IoT device streaming |
| 422 | |
| 423 | **Do not use WebRTC for:** Standard server-to-client push (use SSE or WebSocket), REST API alternatives, or scenarios where a server relay is needed anyway. |
| 424 | |
| 425 | ## Connection Management Best Practices |
| 426 | |
| 427 | ### Exponential backoff with jitter |
| 428 | |
| 429 | When reconnecting after a failure, exponential backoff prevents thundering herd problems: |
| 430 | |
| 431 | |
| 432 | function getReconnectDelay(attempt, baseDelay = 1000, maxDelay = 30000) { |
| 433 | const exponentialDelay = baseDelay * Math.pow(2, attempt); |
| 434 | const cappedDelay = Math.min(exponentialDelay, maxDelay); |
| 435 | // Add random jitter (0-100% of delay) to prevent synchronized reconnects |
| 436 | const jitter = cappedDelay * Math.random(); |
| 437 | return cappedDelay + jitter; |
| 438 | } |
| 439 | |
| 440 | |
| 441 | Without jitter, if a server restarts and 10,000 clients all reconnect simultaneously with the same backoff timing, the server faces a thundering herd of reconnection attempts. |
| 442 | |
| 443 | ### Message queuing during disconnection |
| 444 | |
| 445 | Queue outbound messages when disconnected and flush when reconnected: |
| 446 | |
| 447 | |
| 448 | class ResilientConnection { |
| 449 | constructor() { |
| 450 | this.queue = []; |
| 451 | this.connected = false; |
| 452 | } |
| 453 | |
| 454 | send(message) { |
| 455 | if (this.connected) { |
| 456 | this.transport.send(message); |
| 457 | } else { |
| 458 | this.queue.push(message); |
| 459 | if (this.queue.length > 1000) { |
| 460 | this.queue.shift(); // Prevent unbounded growth |
| 461 | } |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | onReconnect() { |
| 466 | this.connected = true; |
| 467 | while (this.queue.length > 0) { |
| 468 | this.transport.send(this.queue.shift()); |
| 469 | } |
| 470 | } |
| 471 | } |
| 472 | |
| 473 | |
| 474 | ### Connection lifecycle on mobile |
| 475 | |
| 476 | Mobile connections face unique challenges: |
| 477 | **Network switches:** WiFi to cellular transitions drop TCP connections |
| 478 | **Background tabs:** Browsers throttle or suspend timers and connections in background tabs |
| 479 | **Battery optimization:** OS may terminate background network activity |
| 480 | |
| 481 | Strategies: |
| 482 | Detect visibility changes with `document.visibilitychange` and reconnect when returning to foreground |
| 483 | Use shorter heartbeat intervals on mobile to detect dead connections faster |
| 484 | Implement message ID tracking so the server can resume from the last acknowledged message |
| 485 | |
| 486 | |
| 487 | document.addEventListener('visibilitychange', () => { |
| 488 | if (document.visibilityState === 'visible') { |
| 489 | // Tab became visible -- check connection health |
| 490 | if (!isConnectionAlive()) { |
| 491 | reconnect(); |
| 492 | } |
| 493 | } |
| 494 | }); |
| 495 | |
| 496 | |
| 497 | ## Scaling Real-Time Systems |
| 498 | |
| 499 | ### Horizontal scaling with pub/sub |
| 500 | |
| 501 | WebSocket connections are stateful -- a client connects to a specific server instance. To scale horizontally, use a pub/sub broker: |
| 502 | |
| 503 | |
| 504 | Client A → Server 1 ←→ Redis Pub/Sub ←→ Server 2 → Client B |
| 505 | Client C → Server 1 ←→ or NATS ←→ Server 3 → Client D |
| 506 | |
| 507 | |
| 508 | When Client A sends a message, Server 1 publishes it to Redis. All servers subscribed to that channel receive it and forward to their connected clients. |
| 509 | |
| 510 | ### Connection limits |
| 511 | |
| 512 | Each WebSocket connection consumes: |
| 513 | A file descriptor on the server |
| 514 | Memory for the connection state |
| 515 | CPU for message processing |
| 516 | |
| 517 | Practical limits: |
| 518 | A single Node.js process can handle ~50,000-100,000 concurrent WebSocket connections (depending on message rate) |
| 519 | Use connection limits and load balancing to distribute connections |
| 520 | Consider connection pooling for services that broadcast to many clients |
| 521 | |
| 522 | ### Load balancing considerations |
| 523 | |
| 524 | **Sticky sessions:** Required for WebSocket and long polling (the same client must reach the same server) |
| 525 | **Connection draining:** When scaling down, drain connections gracefully before terminating a server instance |
| 526 | **Health checks:** Exclude servers with too many connections or high latency from the load balancer pool |
| 527 | |
| 528 | The choice of real-time transport should be driven by requirements, not technology preferences. SSE handles most server-push use cases with far less complexity than WebSocket. WebSocket is essential for bidirectional, low-latency communication. WebRTC is specialized for peer-to-peer media. Long polling is the universal fallback. |
| 529 |
Details
- Updated
- Sep 10, 2026
- License
- MIT
- Stars on the repo
- ★ 2,235
- Checked
- Source
- wondelai/skills ↗
- Creator
- wondelai · 319 listings
Similar skills
See all →
SkillCross-Platform Reverse Engineer
SkillBrainstorm Ideas
SkillCode Review Feedback Analyzer
SkillBug Diagnosis Framework
SkillImplement Spec
SkillPR Description WriterMore from wondelai
SkillSoftware Design Philosophy
SkillActionable Metrics Guide
SkillAdvertising Media
SkillAffordance Design
SkillLean UX in Agile Teams
SkillAMP Framework Practitioner