Real time communication skill

When data must flow continuously between client and server, the standard HTTP request-response model introduces unnecessary overhead.…

by wondelai·MIT license·★ 2,235 Stars on the repo·GitHub ↗

Use now

Files of Real time communication

wondelai/main1 file
real-time-communication.md
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

  1. Choosing the Right Approach
  2. WebSocket Protocol
  3. Server-Sent Events (SSE)
  4. Long Polling
  5. WebRTC Basics
  6. Connection Management Best Practices
  7. 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 EventSource API 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 as Last-Event-ID on 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
  1. Client sends an HTTP request
  2. Server holds the request open until it has new data (or a timeout occurs)
  3. Server sends the response
  4. Client immediately sends a new request
  5. 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 ──┘
  1. Signaling: Peers exchange session descriptions (SDP) and ICE candidates through a signaling server (WebSocket, HTTP, or any mechanism)
  2. ICE: Interactive Connectivity Establishment discovers the best network path (direct, STUN, or TURN relay)
  3. 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.visibilitychange and 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 
3When 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
71. [Choosing the Right Approach](#choosing-the-right-approach)
82. [WebSocket Protocol](#websocket-protocol)
93. [Server-Sent Events (SSE)](#server-sent-events-sse)
104. [Long Polling](#long-polling)
115. [WebRTC Basics](#webrtc-basics)
126. [Connection Management Best Practices](#connection-management-best-practices)
137. [Scaling Real-Time Systems](#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 
31WebSocket 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```
361. Client sends HTTP upgrade request
372. Server responds with 101 Switching Protocols
383. Connection upgraded to WebSocket (persistent, full-duplex)
394. Client and server exchange frames freely
405. Either side sends a close frame to terminate
41```
42 
43### Opening handshake
44 
45The WebSocket connection begins as an HTTP request:
46 
47```
48GET /ws HTTP/1.1
49Host: example.com
50Upgrade: websocket
51Connection: Upgrade
52Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
53Sec-WebSocket-Version: 13
54```
55 
56Server response:
57```
58HTTP/1.1 101 Switching Protocols
59Upgrade: websocket
60Connection: Upgrade
61Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
62```
63 
64After this handshake, the connection is no longer HTTP -- it is a raw TCP connection with WebSocket framing.
65 
66### Client implementation
67 
68```javascript
69class 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 
143Mobile networks and load balancers silently drop idle connections. Heartbeats detect dead connections:
144 
145```javascript
146// Client-side heartbeat
147class 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 
192An 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 
194RFC 8441 defines WebSocket over HTTP/2, but browser support is limited.
195 
196### Binary vs. text frames
197 
198For high-throughput applications, binary frames with a compact serialization format (Protocol Buffers, MessagePack, CBOR) significantly reduce message size:
199 
200```javascript
201// Text (JSON) - 84 bytes
202ws.send(JSON.stringify({ type: 'position', x: 123.456, y: 789.012, t: 1679000000 }));
203 
204// Binary (Protocol Buffers) - ~20 bytes
205const buffer = Position.encode({ x: 123.456, y: 789.012, t: 1679000000 }).finish();
206ws.send(buffer);
207```
208 
209## Server-Sent Events (SSE)
210 
211SSE 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 
223SSE 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 
233The server sends a response with `Content-Type: text/event-stream`:
234 
235```
236HTTP/1.1 200 OK
237Content-Type: text/event-stream
238Cache-Control: no-cache
239Connection: keep-alive
240 
241data: {"message": "Hello"}
242 
243event: notification
244data: {"type": "alert", "text": "New message"}
245 
246id: 42
247event: update
248data: {"price": 142.50}
249 
250retry: 5000
251 
252```
253 
254Event 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```javascript
263const eventSource = new EventSource('/api/stream');
264 
265// Default "message" events
266eventSource.onmessage = (event) => {
267 const data = JSON.parse(event.data);
268 console.log('Message:', data);
269};
270 
271// Named events
272eventSource.addEventListener('notification', (event) => {
273 const data = JSON.parse(event.data);
274 showNotification(data);
275});
276 
277// Connection management
278eventSource.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
287eventSource.close();
288```
289 
290### Resuming after disconnection
291 
292When the connection drops, the browser sends the last received event ID:
293 
294```
295GET /api/stream HTTP/1.1
296Last-Event-ID: 42
297```
298 
299The 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```javascript
306// Option 1: Token in URL (less secure, logged in server access logs)
307const 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
313async 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 
330Long polling is the fallback when WebSocket and SSE are unavailable (restrictive firewalls, legacy infrastructure).
331 
332### How it works
333 
3341. Client sends an HTTP request
3352. Server holds the request open until it has new data (or a timeout occurs)
3363. Server sends the response
3374. Client immediately sends a new request
3385. Repeat
339 
340```javascript
341async 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 
378WebRTC enables peer-to-peer communication for audio, video, and arbitrary data between browsers without a relay server.
379 
380### Architecture
381 
382```
383Browser A ←→ Signaling Server ←→ Browser B
384 ↕ ↕
385 └────── Direct P2P Connection ──┘
386```
387 
3881. **Signaling:** Peers exchange session descriptions (SDP) and ICE candidates through a signaling server (WebSocket, HTTP, or any mechanism)
3892. **ICE:** Interactive Connectivity Establishment discovers the best network path (direct, STUN, or TURN relay)
3903. **DTLS/SRTP:** Encrypted media and data channels over UDP
391 
392### Data channels
393 
394For 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```javascript
401const peerConnection = new RTCPeerConnection({
402 iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
403});
404 
405const dataChannel = peerConnection.createDataChannel('game', {
406 ordered: false, // Unordered for lower latency
407 maxRetransmits: 0, // Unreliable (no retransmission)
408});
409 
410dataChannel.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 
429When reconnecting after a failure, exponential backoff prevents thundering herd problems:
430 
431```javascript
432function 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 
441Without 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 
445Queue outbound messages when disconnected and flush when reconnected:
446 
447```javascript
448class 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 
476Mobile 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 
481Strategies:
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```javascript
487document.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 
501WebSocket connections are stateful -- a client connects to a specific server instance. To scale horizontally, use a pub/sub broker:
502 
503```
504Client A → Server 1 ←→ Redis Pub/Sub ←→ Server 2 → Client B
505Client C → Server 1 ←→ or NATS ←→ Server 3 → Client D
506```
507 
508When 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 
512Each WebSocket connection consumes:
513- A file descriptor on the server
514- Memory for the connection state
515- CPU for message processing
516 
517Practical 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 
528The 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 

Discussion