|$ curl https://forge-ai.dev/api/markdown?path=docs/js/websockets
$cat docs/javascript-—-websockets.md
updated This week·40 min read·published

JavaScript — WebSockets

JavaScriptNetworkingReal-timeIntermediate to Advanced
Introduction

WebSockets provide a full-duplex communication channel over a single, long-lived TCP connection. Unlike HTTP, which follows a request-response pattern, WebSockets allow the server to push data to the client at any time without the client polling. This makes them ideal for real-time applications like chat, live feeds, gaming, and collaborative editing.

A WebSocket connection begins as an HTTP handshake (Upgrade header), then the protocol switches from HTTP to the WebSocket protocol (ws:// or wss://). Once established, both client and server can send messages independently. The connection remains open until one side closes it explicitly or the network fails.

The WebSocket API is part of the HTML Living Standard and is supported in all modern browsers. On the server side, implementations exist for Node.js (ws, Socket.IO), Python (websockets, Django Channels), Go (gorilla/websocket), and most other languages. The wss:// scheme provides encrypted connections (WebSocket over TLS).

ws-intro.js
JavaScript
1// Basic WebSocket connection
2const socket = new WebSocket('wss://example.com/ws');
3
4// Connection opened
5socket.addEventListener('open', (event) => {
6 console.log('Connected to server');
7 socket.send('Hello Server!');
8});
9
10// Listen for messages
11socket.addEventListener('message', (event) => {
12 console.log('Message from server:', event.data);
13});
14
15// Listen for errors
16socket.addEventListener('error', (event) => {
17 console.error('WebSocket error:', event);
18});
19
20// Listen for connection close
21socket.addEventListener('close', (event) => {
22 console.log('Disconnected:', event.code, event.reason);
23});
24
25// Close the connection
26socket.close(1000, 'Client closing'); // 1000 = Normal Closure
Connection Lifecycle

A WebSocket connection moves through several states during its lifetime. The readyState property reflects the current state. The lifecycle begins with the HTTP upgrade handshake and ends when the connection is closed for any reason. Each state transition fires corresponding events.

StateConstantValueDescription
CONNECTINGWebSocket.CONNECTING0Handshake in progress, not yet open
OPENWebSocket.OPEN1Connection established, ready to send/receive
CLOSINGWebSocket.CLOSING2Close handshake in progress
CLOSEDWebSocket.CLOSED3Connection closed or failed to open
ws-lifecycle.js
JavaScript
1// Connection lifecycle management
2class ConnectionManager {
3 constructor(url) {
4 this.url = url;
5 this.socket = null;
6 this.listeners = new Map();
7 }
8
9 connect() {
10 if (this.socket?.readyState === WebSocket.OPEN) {
11 console.log('Already connected');
12 return;
13 }
14
15 console.log(`[WS] Connecting to ${this.url}...`);
16 this.socket = new WebSocket(this.url);
17
18 this.socket.addEventListener('open', (event) => {
19 console.log(`[WS] Connected (state: ${this.socket.readyState})`);
20 this.emit('connected', event);
21 });
22
23 this.socket.addEventListener('message', (event) => {
24 this.emit('message', event.data);
25 });
26
27 this.socket.addEventListener('close', (event) => {
28 console.log(`[WS] Closed: code=${event.code} reason="${event.reason}"`);
29 this.emit('disconnected', event);
30 this.socket = null;
31 });
32
33 this.socket.addEventListener('error', (event) => {
34 console.error('[WS] Error:', event);
35 this.emit('error', event);
36 });
37 }
38
39 disconnect(code = 1000, reason = 'Client disconnect') {
40 if (this.socket) {
41 console.log(`[WS] Disconnecting: ${reason}`);
42 this.socket.close(code, reason);
43 }
44 }
45
46 on(event, callback) {
47 if (!this.listeners.has(event)) {
48 this.listeners.set(event, []);
49 }
50 this.listeners.get(event).push(callback);
51 }
52
53 emit(event, data) {
54 const callbacks = this.listeners.get(event) || [];
55 callbacks.forEach(cb => cb(data));
56 }
57}
send() & Message Events

The send() method transmits data to the server. You can send strings, ArrayBuffers, TypedArrays, or Blobs. The message event fires when data arrives from the server. Messages are received in the order they were sent, but there is no built-in acknowledgment mechanism — you must implement your own if needed.

ws-send.js
JavaScript
1// Sending different data types
2const socket = new WebSocket('wss://example.com/ws');
3
4// Text message (most common)
5socket.send('Hello, server!');
6socket.send(JSON.stringify({ type: 'greeting', payload: { name: 'John' } }));
7
8// Binary data
9const buffer = new ArrayBuffer(8);
10const view = new DataView(buffer);
11view.setInt32(0, 12345);
12view.setFloat32(4, 3.14);
13socket.send(buffer);
14
15// Blob
16const blob = new Blob(['binary data'], { type: 'application/octet-stream' });
17socket.send(blob);
18
19// Receiving messages
20socket.addEventListener('message', (event) => {
21 if (event.data instanceof ArrayBuffer) {
22 // Binary message
23 const view = new DataView(event.data);
24 console.log('Binary message:', view.getInt32(0));
25 } else if (event.data instanceof Blob) {
26 // Blob message — read with FileReader
27 const reader = new FileReader();
28 reader.onload = () => console.log('Blob:', reader.result);
29 reader.readAsText(event.data);
30 } else {
31 // Text message — try parsing as JSON
32 try {
33 const data = JSON.parse(event.data);
34 console.log('JSON message:', data);
35 } catch {
36 console.log('Text message:', event.data);
37 }
38 }
39});
40
41// Check binary type before connection
42socket.binaryType = 'arraybuffer'; // or 'blob' (default)
43// Setting this determines how binary data is received
Binary vs Text Frames

WebSocket messages are transmitted as frames. Each frame can be either text (UTF-8) or binary. The WebSocket protocol automatically frames messages, but the distinction matters for interpretation. Text frames are decoded as DOMString (JavaScript string), while binary frames arrive as either Blob or ArrayBuffer depending on the binaryType setting.

Frame TypeJavaScript Type (receive)send() ArgumentUse Case
TextstringstringJSON, XML, plain text messages
BinaryBlob or ArrayBufferArrayBuffer, TypedArray, BlobFiles, images, protocol buffers, audio
ws-binary-text.js
JavaScript
1// Configuring binary type
2const socket = new WebSocket('wss://example.com/ws');
3
4// 'blob' is the default — provides a Blob object
5socket.binaryType = 'blob';
6// 'arraybuffer' provides an ArrayBuffer
7socket.binaryType = 'arraybuffer';
8
9// When to use each binary type:
10// Blob: larger payloads, file-like data, streaming to <img> or <video>
11// ArrayBuffer: small binary protocols, WebAssembly, crypto, TypedArray processing
12
13// Example: receiving binary protocol messages
14socket.binaryType = 'arraybuffer';
15
16socket.addEventListener('message', (event) => {
17 if (typeof event.data === 'string') {
18 // Text frame — parse as JSON
19 const msg = JSON.parse(event.data);
20 handleTextMessage(msg);
21 } else {
22 // Binary frame — parse structured binary protocol
23 const buffer = event.data;
24 const view = new DataView(buffer);
25
26 const messageType = view.getUint8(0);
27 const sequenceId = view.getUint32(1, false); // big-endian
28 const payloadSize = view.getUint16(5, false);
29
30 switch (messageType) {
31 case 0x01: handlePositionUpdate(view, 7); break;
32 case 0x02: handleAudioData(view, 7); break;
33 default: console.warn('Unknown message type:', messageType);
34 }
35 }
36});
37
38// Sending binary data efficiently
39function sendBinary(socket, type, payload) {
40 const headerSize = 3; // type(1) + length(2)
41 const buffer = new ArrayBuffer(headerSize + payload.byteLength);
42 const view = new Uint8Array(buffer);
43
44 view[0] = type;
45 view[1] = (payload.byteLength >> 8) & 0xFF;
46 view[2] = payload.byteLength & 0xFF;
47 view.set(new Uint8Array(payload), headerSize);
48
49 socket.send(buffer);
50}
Reconnection Strategies

WebSocket connections can drop due to network issues, server restarts, or idle timeouts. A robust application needs automatic reconnection with exponential backoff to avoid overwhelming the server. The reconnection strategy should include configurable retry limits, backoff factors, and jitter to prevent thundering herd problems.

ws-reconnect.js
JavaScript
1// Robust reconnection with exponential backoff
2class ReconnectingWebSocket {
3 constructor(url, options = {}) {
4 this.url = url;
5 this.maxRetries = options.maxRetries ?? Infinity;
6 this.baseDelay = options.baseDelay ?? 1000;
7 this.maxDelay = options.maxDelay ?? 30000;
8 this.backoffFactor = options.factor ?? 2;
9 this.jitter = options.jitter ?? true;
10 this.onMessage = options.onMessage || (() => {});
11 this.onStatusChange = options.onStatusChange || (() => {});
12
13 this.retryCount = 0;
14 this.intentionalClose = false;
15 this.socket = null;
16 this.connect();
17 }
18
19 connect() {
20 if (this.intentionalClose) return;
21
22 this.socket = new WebSocket(this.url);
23
24 this.socket.addEventListener('open', () => {
25 this.retryCount = 0;
26 this.onStatusChange('connected');
27 });
28
29 this.socket.addEventListener('message', (event) => {
30 this.onMessage(event.data);
31 });
32
33 this.socket.addEventListener('close', () => {
34 this.onStatusChange('disconnected');
35 this.socket = null;
36 this.scheduleReconnect();
37 });
38
39 this.socket.addEventListener('error', () => {
40 // 'close' will fire after 'error'
41 });
42 }
43
44 scheduleReconnect() {
45 if (this.intentionalClose) return;
46 if (this.retryCount >= this.maxRetries) {
47 this.onStatusChange('failed');
48 return;
49 }
50
51 const delay = Math.min(
52 this.baseDelay * Math.pow(this.backoffFactor, this.retryCount),
53 this.maxDelay
54 );
55
56 // Add jitter: random ±25% of delay
57 const jittered = this.jitter
58 ? delay * (0.75 + Math.random() * 0.5)
59 : delay;
60
61 this.retryCount++;
62 this.onStatusChange(`reconnecting in ${Math.round(jittered)}ms`);
63
64 setTimeout(() => this.connect(), jittered);
65 }
66
67 send(data) {
68 if (this.socket?.readyState === WebSocket.OPEN) {
69 this.socket.send(data);
70 } else {
71 console.warn('Cannot send — socket not open');
72 }
73 }
74
75 close() {
76 this.intentionalClose = true;
77 if (this.socket) {
78 this.socket.close(1000, 'Client closing');
79 this.socket = null;
80 }
81 }
82}
83
84// Usage
85const ws = new ReconnectingWebSocket('wss://example.com/ws', {
86 maxRetries: 10,
87 baseDelay: 500,
88 maxDelay: 16000,
89 onMessage: (data) => console.log('Received:', data),
90 onStatusChange: (status) => updateConnectionUI(status),
91});
🔥

pro tip

Always use exponential backoff with jitter for reconnection. Without jitter, if multiple clients disconnect simultaneously (e.g., server restart), they all reconnect at the same time — creating a thundering herd. Jitter spreads the reconnection attempts randomly. Set a reasonable maxRetries to avoid infinite reconnection loops.
Heartbeat / Ping-Pong

WebSocket connections can remain open indefinitely, but intermediate proxies, load balancers, and firewalls may close idle connections. A heartbeat mechanism (ping-pong) keeps the connection alive by sending periodic small messages. The WebSocket protocol has built-in ping/pong frames, but the browser API does not expose them — you must implement heartbeat at the application level.

ws-heartbeat.js
JavaScript
1// Heartbeat to keep connection alive
2class HeartbeatWS {
3 constructor(url, options = {}) {
4 this.url = url;
5 this.pingInterval = options.pingInterval ?? 30000; // 30s
6 this.pongTimeout = options.pongTimeout ?? 10000; // 10s
7 this.onDead = options.onDead || (() => {});
8 this.socket = null;
9 this.pingTimer = null;
10 this.pongTimer = null;
11 this.isAlive = false;
12 }
13
14 connect() {
15 this.socket = new WebSocket(this.url);
16
17 this.socket.addEventListener('open', () => {
18 this.isAlive = true;
19 this.startPing();
20 });
21
22 this.socket.addEventListener('message', (event) => {
23 try {
24 const msg = JSON.parse(event.data);
25 if (msg.type === 'pong') {
26 this.handlePong();
27 return; // don't emit pong to message handlers
28 }
29 } catch {}
30 this.handleMessage(event.data);
31 });
32
33 this.socket.addEventListener('close', () => {
34 this.stopPing();
35 this.isAlive = false;
36 });
37
38 this.socket.addEventListener('error', () => {
39 this.isAlive = false;
40 });
41 }
42
43 startPing() {
44 this.pingTimer = setInterval(() => {
45 if (this.socket?.readyState === WebSocket.OPEN) {
46 this.socket.send(JSON.stringify({ type: 'ping', time: Date.now() }));
47
48 // Set pong timeout
49 this.pongTimer = setTimeout(() => {
50 console.warn('Pong not received — assuming dead');
51 this.isAlive = false;
52 this.onDead();
53 this.socket?.close();
54 }, this.pongTimeout);
55 }
56 }, this.pingInterval);
57 }
58
59 handlePong() {
60 clearTimeout(this.pongTimer);
61 this.isAlive = true;
62 }
63
64 stopPing() {
65 clearInterval(this.pingTimer);
66 clearTimeout(this.pongTimer);
67 }
68
69 send(data) {
70 if (this.socket?.readyState === WebSocket.OPEN) {
71 this.socket.send(data);
72 }
73 }
74
75 close() {
76 this.stopPing();
77 this.socket?.close();
78 }
79}
80
81// Simpler: server-side ping, client responds with pong
82socket.addEventListener('message', (event) => {
83 try {
84 const msg = JSON.parse(event.data);
85 if (msg.type === 'ping') {
86 socket.send(JSON.stringify({ type: 'pong', id: msg.id }));
87 return;
88 }
89 } catch {}
90 // Normal message handling
91 handleMessage(event.data);
92});
Authentication

WebSocket connections cannot use traditional HTTP authentication headers during the upgrade handshake from within the browser. Common approaches include passing tokens as query parameters, using the subprotocol field, or authenticating after connection establishment. Each approach has security trade-offs.

ws-auth.js
JavaScript
1// Authentication strategies
2
3// 1. Token in URL query parameter (most common)
4const token = await getAuthToken();
5const socket = new WebSocket(`wss://example.com/ws?token=${token}`);
6// Server extracts token from the upgrade request URL
7// WARNING: token may be logged in server access logs
8
9// 2. Auth in subprotocol field
10const socket = new WebSocket('wss://example.com/ws', [
11 `auth.${token}`
12]);
13// Server checks the Sec-WebSocket-Protocol header
14
15// 3. Auth after connection (send auth message first)
16const socket = new WebSocket('wss://example.com/ws');
17
18socket.addEventListener('open', () => {
19 // Send authentication immediately after connecting
20 socket.send(JSON.stringify({
21 type: 'auth',
22 token: authToken,
23 clientId: generateClientId(),
24 }));
25});
26
27socket.addEventListener('message', (event) => {
28 const msg = JSON.parse(event.data);
29
30 if (msg.type === 'auth_ok') {
31 console.log('Authenticated, session:', msg.sessionId);
32 socket.isAuthenticated = true;
33 return;
34 }
35
36 if (msg.type === 'auth_error') {
37 console.error('Auth failed:', msg.reason);
38 socket.close(4001, 'Authentication failed');
39 return;
40 }
41
42 // Only process messages after authentication
43 if (socket.isAuthenticated) {
44 handleMessage(msg);
45 }
46});
47
48// 4. Cookie-based auth
49// If the server set a session cookie on the origin page,
50// the cookie is automatically sent during the WebSocket
51// handshake (same-origin or with credentials config).
52const socket = new WebSocket('wss://example.com/ws');
53// Browser automatically includes cookies for the domain

warning

Passing tokens in query parameters is convenient but exposes the token in server access logs, browser history, and the Referer header. For sensitive applications, use the auth after connect approach or implement a short-lived handshake token that is exchanged for a session. Always use wss:// (TLS) to prevent token interception.
WebSocket vs SSE vs Polling

Choosing the right real-time communication protocol depends on your use case. WebSockets provide full-duplex communication. Server-Sent Events (SSE) allow the server to push data to the client over HTTP. Polling is the simplest approach but the least efficient. Each has trade-offs in complexity, browser support, and resource usage.

FeatureWebSocketSSE (EventSource)Polling
DirectionFull-duplexServer → Client onlyClient → Server (pull)
Protocolws:// / wss://HTTP (standard)HTTP (standard)
Message FormatText or BinaryText only (UTF-8)Any (HTTP response)
Auto-reconnect✗ (manual)✓ (built-in)✗ (manual)
Browser SupportAll modernAll modern (except IE)Universal
ComplexityHigherLowerSimple
LatencyLow (real-time)Low (real-time)Depends on interval
Best ForChat, gaming, collaborative editingLive feeds, notifications, stock tickersSimple updates, legacy systems, low frequency
ws-vs-sse-polling.js
JavaScript
1// Server-Sent Events (SSE) — one-way push
2const eventSource = new EventSource('/api/events');
3
4eventSource.addEventListener('open', () => {
5 console.log('SSE connection opened');
6});
7
8eventSource.addEventListener('message', (event) => {
9 const data = JSON.parse(event.data);
10 console.log('SSE message:', data);
11});
12
13eventSource.addEventListener('error', (event) => {
14 console.error('SSE error:', event);
15});
16
17// Named events
18eventSource.addEventListener('user-update', (event) => {
19 console.log('User update:', JSON.parse(event.data));
20});
21
22eventSource.close(); // stop listening
23
24// Polling approach
25async function startPolling(interval = 5000) {
26 const poll = async () => {
27 try {
28 const response = await fetch('/api/updates');
29 const data = await response.json();
30 handleUpdates(data);
31 } catch (error) {
32 console.error('Poll failed:', error);
33 }
34 };
35
36 await poll(); // immediate first poll
37 return setInterval(poll, interval);
38}
39
40// Stop polling
41const pollId = await startPolling(3000);
42clearInterval(pollId);
43
44// When to use WebSocket:
45// - Bidirectional communication (chat, gaming, collaborative tools)
46// - Binary data streaming (audio, video, game state)
47// - Low-latency requirements (< 100ms)
48// - Server-initiated messages at high frequency
49
50// When to use SSE:
51// - One-way server-to-client updates
52// - Simple implementation (standard HTTP)
53// - Automatic reconnection is desired
54// - Firebase-like real-time listeners
55
56// When to use Polling:
57// - Simple, infrequent updates (every 30+ seconds)
58// - Behind restrictive firewalls/proxies
59// - No persistent connection overhead needed

best practice

Start with SSE if you only need server-to-client messages — it is simpler, uses standard HTTP, and has built-in reconnection. Use WebSockets when you need bidirectional communication or binary data. Avoid polling for real-time use cases; it wastes bandwidth and battery, especially on mobile devices.
Buffering Messages

When the WebSocket connection is not yet open, calling send() throws an error. A common pattern is to buffer outgoing messages in a queue while the connection is establishing, then flush them once the connection opens. This ensures no messages are lost during the initial handshake or reconnection.

ws-buffering.js
JavaScript
1// Buffered WebSocket with automatic flush
2class BufferedWebSocket {
3 constructor(url) {
4 this.url = url;
5 this.buffer = [];
6 this.socket = null;
7 this.connect();
8 }
9
10 connect() {
11 this.socket = new WebSocket(this.url);
12
13 this.socket.addEventListener('open', () => {
14 console.log(`Flushing ${this.buffer.length} buffered messages`);
15 // Flush all buffered messages
16 while (this.buffer.length > 0) {
17 const data = this.buffer.shift();
18 this.socket.send(data);
19 }
20 });
21
22 this.socket.addEventListener('message', (event) => {
23 this.onMessage?.(event.data);
24 });
25
26 this.socket.addEventListener('close', () => {
27 this.socket = null;
28 });
29 }
30
31 send(data) {
32 if (this.socket?.readyState === WebSocket.OPEN) {
33 // Connection is open — send immediately
34 this.socket.send(data);
35 } else if (this.socket?.readyState === WebSocket.CONNECTING) {
36 // Connection is still opening — buffer
37 console.log('Buffering message while connecting');
38 this.buffer.push(data);
39 } else {
40 // Connection is closed — try reconnecting
41 console.warn('Connection closed, reconnecting...');
42 this.buffer.push(data);
43 this.connect();
44 }
45 }
46
47 onMessage(data) {
48 // Override this in subclass or set callback
49 console.log('Received:', data);
50 }
51
52 close() {
53 this.buffer = [];
54 this.socket?.close();
55 }
56}
57
58// Queue-based approach with priority
59class PriorityBufferedWS extends BufferedWebSocket {
60 send(data, priority = 0) {
61 const message = { data, priority, timestamp: Date.now() };
62
63 if (this.socket?.readyState === WebSocket.OPEN) {
64 this.socket.send(data);
65 } else {
66 // Insert in priority order (higher = sooner)
67 const idx = this.buffer.findIndex(m => m.priority < priority);
68 if (idx === -1) {
69 this.buffer.push(message);
70 } else {
71 this.buffer.splice(idx, 0, message);
72 }
73 }
74 }
75
76 flush() {
77 // Sort by priority then timestamp
78 this.buffer.sort((a, b) =>
79 b.priority - a.priority || a.timestamp - b.timestamp
80 );
81 super.flush();
82 }
83}
84
85// Usage
86const ws = new BufferedWebSocket('wss://example.com/ws');
87
88// These will be buffered if connection isn't open yet
89ws.send(JSON.stringify({ type: 'join', room: 'general' }));
90ws.send(JSON.stringify({ type: 'message', text: 'Hello!' }));
91
92// Once connection opens, buffer is flushed automatically
Practical: Chat Application

A real-time chat application is the canonical WebSocket use case. It requires connection management, message serialization, typing indicators, and presence awareness. Below is a complete chat client implementation.

chat-client.js
JavaScript
1// Chat client class
2class ChatClient {
3 constructor(serverUrl, username) {
4 this.username = username;
5 this.serverUrl = serverUrl;
6 this.socket = null;
7 this.rooms = new Set();
8 this.messageHandlers = [];
9 this.typingTimers = new Map();
10 }
11
12 connect() {
13 this.socket = new ReconnectingWebSocket(this.serverUrl, {
14 maxRetries: 20,
15 baseDelay: 500,
16 onMessage: (data) => this.handleMessage(data),
17 onStatusChange: (status) => {
18 console.log('Chat status:', status);
19 this.notifyHandlers('status', { status });
20 },
21 });
22
23 // Send join message once connected
24 this.socket.addEventListener('open', () => {
25 this.send({ type: 'join', username: this.username, rooms: [...this.rooms] });
26 });
27 }
28
29 joinRoom(roomId) {
30 this.rooms.add(roomId);
31 this.send({ type: 'join_room', room: roomId });
32 }
33
34 leaveRoom(roomId) {
35 this.rooms.delete(roomId);
36 this.send({ type: 'leave_room', room: roomId });
37 }
38
39 sendMessage(roomId, text) {
40 this.send({
41 type: 'message',
42 room: roomId,
43 text,
44 username: this.username,
45 timestamp: Date.now(),
46 });
47 }
48
49 sendTyping(roomId, isTyping) {
50 this.send({ type: 'typing', room: roomId, username: this.username, isTyping });
51 }
52
53 send(data) {
54 this.socket.send(JSON.stringify(data));
55 }
56
57 onMessage(handler) {
58 this.messageHandlers.push(handler);
59 }
60
61 handleMessage(rawData) {
62 try {
63 const msg = JSON.parse(rawData);
64
65 switch (msg.type) {
66 case 'message':
67 this.notifyHandlers('message', msg);
68 break;
69 case 'typing':
70 this.handleTypingIndicator(msg);
71 break;
72 case 'user_joined':
73 this.notifyHandlers('user_joined', msg);
74 break;
75 case 'user_left':
76 this.notifyHandlers('user_left', msg);
77 break;
78 case 'room_history':
79 this.notifyHandlers('history', msg.messages);
80 break;
81 }
82 } catch (error) {
83 console.error('Failed to parse message:', error);
84 }
85 }
86
87 handleTypingIndicator(msg) {
88 if (msg.username === this.username) return;
89
90 if (msg.isTyping) {
91 this.notifyHandlers('typing_start', msg);
92 // Auto-clear typing after 3 seconds of no updates
93 if (this.typingTimers.has(msg.username)) {
94 clearTimeout(this.typingTimers.get(msg.username));
95 }
96 this.typingTimers.set(msg.username, setTimeout(() => {
97 this.notifyHandlers('typing_stop', msg);
98 }, 3000));
99 } else {
100 clearTimeout(this.typingTimers.get(msg.username));
101 this.notifyHandlers('typing_stop', msg);
102 }
103 }
104
105 notifyHandlers(event, data) {
106 this.messageHandlers.forEach(h => h(event, data));
107 }
108
109 disconnect() {
110 this.send({ type: 'leave', username: this.username });
111 this.socket.close(1000, 'Client leaving');
112 }
113}
114
115// Usage
116const chat = new ChatClient('wss://chat.example.com/ws', 'John');
117
118chat.onMessage((event, data) => {
119 switch (event) {
120 case 'message':
121 renderMessage(data);
122 break;
123 case 'typing_start':
124 showTypingIndicator(data.username);
125 break;
126 case 'typing_stop':
127 hideTypingIndicator(data.username);
128 break;
129 case 'user_joined':
130 addUserToRoster(data);
131 break;
132 case 'status':
133 updateConnectionStatus(data.status);
134 break;
135 }
136});
137
138chat.connect();
139chat.joinRoom('general');
140chat.sendMessage('general', 'Hello everyone!');
Practical: Real-Time Data Feed

Real-time data feeds are essential for dashboards, financial applications, live sports scores, and IoT monitoring. WebSockets enable sub-second updates without polling overhead. Below is a pattern for subscribing to multiple data channels and processing updates efficiently.

data-feed.js
JavaScript
1// Real-time data feed client
2class DataFeed {
3 constructor(url) {
4 this.url = url;
5 this.socket = null;
6 this.subscriptions = new Map(); // channel -> Set of callbacks
7 this.reconnectAttempts = 0;
8 this.connect();
9 }
10
11 connect() {
12 this.socket = new WebSocket(this.url);
13
14 this.socket.addEventListener('open', () => {
15 this.reconnectAttempts = 0;
16 // Re-subscribe to all channels
17 for (const channel of this.subscriptions.keys()) {
18 this.send({ type: 'subscribe', channel });
19 }
20 });
21
22 this.socket.addEventListener('message', (event) => {
23 const msg = JSON.parse(event.data);
24 const callbacks = this.subscriptions.get(msg.channel);
25 if (callbacks) {
26 callbacks.forEach(cb => cb(msg.data, msg));
27 }
28 });
29
30 this.socket.addEventListener('close', () => {
31 this.scheduleReconnect();
32 });
33
34 this.socket.addEventListener('error', () => {});
35 }
36
37 subscribe(channel, callback) {
38 if (!this.subscriptions.has(channel)) {
39 this.subscriptions.set(channel, new Set());
40 // Subscribe on server if connected
41 if (this.socket?.readyState === WebSocket.OPEN) {
42 this.send({ type: 'subscribe', channel });
43 }
44 }
45 this.subscriptions.get(channel).add(callback);
46
47 // Return unsubscribe function
48 return () => {
49 const cbs = this.subscriptions.get(channel);
50 if (cbs) {
51 cbs.delete(callback);
52 if (cbs.size === 0) {
53 this.subscriptions.delete(channel);
54 this.send({ type: 'unsubscribe', channel });
55 }
56 }
57 };
58 }
59
60 send(data) {
61 if (this.socket?.readyState === WebSocket.OPEN) {
62 this.socket.send(JSON.stringify(data));
63 }
64 }
65
66 scheduleReconnect() {
67 const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
68 this.reconnectAttempts++;
69 setTimeout(() => this.connect(), delay);
70 }
71
72 close() {
73 this.subscriptions.clear();
74 this.socket?.close();
75 }
76}
77
78// Usage
79const feed = new DataFeed('wss://market.example.com/feed');
80
81// Subscribe to stock prices
82const unsubAAPL = feed.subscribe('stock.AAPL', (price) => {
83 updatePrice('AAPL', price);
84});
85
86const unsubGOOG = feed.subscribe('stock.GOOG', (price) => {
87 updatePrice('GOOG', price);
88});
89
90// Subscribe to news alerts
91const unsubNews = feed.subscribe('news.breaking', (article) => {
92 showNotification(article);
93});
94
95// Unsubscribe when no longer needed
96unsubAAPL();
97
98// Batch subscribe for efficiency
99const tickers = ['AAPL', 'GOOG', 'MSFT', 'AMZN', 'TSLA'];
100tickers.forEach(ticker => {
101 feed.subscribe(`stock.${ticker}`, (price) => {
102 updatePrice(ticker, price);
103 });
104});
WebSocket Security

WebSocket security requires attention to several areas: use wss:// to encrypt all traffic (WebSocket over TLS), validate the origin header on the server, implement authentication, and guard against common attacks like Cross-Site WebSocket Hijacking (CSWSH).

Always use wss:// in production — ws:// sends all messages in plaintext
Validate the Origin header on the server to prevent cross-origin attacks
Never trust user input — validate and sanitize all messages server-side
Use short-lived tokens for authentication, not API keys in URLs
Implement rate limiting to prevent DoS attacks
Close connections with appropriate status codes (1000 for normal, 1008 for policy violation)
Avoid sending sensitive data in query parameters (logged in server access logs)
Use subprotocol negotiation to version your WebSocket protocol
ws-security.js
JavaScript
1// Security best practices
2const socket = new WebSocket('wss://api.example.com/ws');
3// ↑ ↑
4// TLS encryption WSS scheme (not ws://)
5
6// Origin validation (server-side pseudocode)
7// Server should check:
8// Origin: https://your-app.com
9// Reject requests from unknown origins
10
11// Input sanitization (client-side defense)
12function sanitizeMessage(text) {
13 return text
14 .replace(/</g, '&lt;')
15 .replace(/>/g, '&gt;')
16 .replace(/"/g, '&quot;')
17 .substring(0, 2000); // limit length
18}
19
20// Handling close codes
21socket.addEventListener('close', (event) => {
22 switch (event.code) {
23 case 1000: // Normal closure
24 break;
25 case 1008: // Policy violation (ban/expired token)
26 console.error('Access revoked:', event.reason);
27 redirectToLogin();
28 break;
29 case 1011: // Server error
30 console.error('Server error, retrying...');
31 break;
32 default:
33 console.warn(`Closed: ${event.code} ${event.reason}`);
34 }
35});
36
37// Rate limiting on client (send side)
38class RateLimitedSocket {
39 constructor(url, maxPerSecond = 10) {
40 this.socket = new WebSocket(url);
41 this.queue = [];
42 this.interval = 1000 / maxPerSecond;
43 this.lastSend = 0;
44 }
45
46 send(data) {
47 const now = Date.now();
48 const elapsed = now - this.lastSend;
49
50 if (elapsed >= this.interval) {
51 this.socket.send(data);
52 this.lastSend = now;
53 } else {
54 setTimeout(() => {
55 this.socket.send(data);
56 this.lastSend = Date.now();
57 }, this.interval - elapsed);
58 }
59 }
60}

danger

Never use ws:// in production. WebSocket messages over unencrypted connections can be intercepted and modified by anyone on the network. Always use wss:// (WebSocket Secure), which encrypts the entire communication channel using TLS. This is especially critical for authentication tokens and user data.
Best Practices
Implement automatic reconnection with exponential backoff and jitter — network interruptions are inevitable
Use a heartbeat (ping/pong) mechanism to detect dead connections — proxies and firewalls close idle connections after ~60s
Buffer messages sent before the connection opens — ensure no messages are lost during the handshake
Always use wss:// in production — unencrypted WebSocket traffic is visible on the network
Authenticate after connection establishment, not via URL parameters — avoid token exposure in logs
Implement rate limiting on both client and server — prevent accidental or malicious flood attacks
Use subprotocol negotiation (second argument to WebSocket) when you need protocol versioning
Parse JSON messages in try/catch — malformed messages should not crash your application
Clean up WebSocket connections when components unmount — prevent zombie connections and memory leaks
Consider SSE instead of WebSocket for one-way server-to-client updates — simpler and with built-in reconnection
$Blueprint — Engineering Documentation·Section ID: JS-WS·Revision: 1.0