|$ curl https://forge-ai.dev/api/markdown?path=docs/web-platform/websockets
$cat docs/websockets.md
updated Recently·30 min read·published

WebSockets

WebSocketsReal-TimeJavaScriptIntermediate to Advanced🎯Free Tools
Introduction

WebSocket is a protocol that provides full-duplex communication over a single TCP connection. Unlike HTTP request-response cycles, WebSockets maintain a persistent connection between client and server, allowing either side to send data at any time with minimal overhead.

WebSockets are ideal for real-time applications like chat systems, live dashboards, multiplayer games, collaborative editing, and streaming data feeds. The protocol starts as an HTTP upgrade handshake, then transitions to a lightweight binary or text frame protocol for the duration of the connection.

WebSocket Basics

The browser API for WebSockets is straightforward. Create a connection with a URL, listen for open/message/error/close events, and send data with the send method. The URL scheme is ws:// for unencrypted or wss:// for encrypted connections.

ws-basics.js
JavaScript
1// Basic WebSocket connection
2const ws = new WebSocket('wss://echo.websocket.org');
3
4ws.addEventListener('open', (event) => {
5 console.log('Connected to server');
6 ws.send('Hello, Server!');
7});
8
9ws.addEventListener('message', (event) => {
10 console.log('Received:', event.data);
11});
12
13ws.addEventListener('close', (event) => {
14 console.log('Disconnected:', event.code, event.reason);
15});
16
17ws.addEventListener('error', (event) => {
18 console.error('WebSocket error:', event);
19});
20
21// Sending different data types
22ws.send('Plain text message');
23
24ws.send(JSON.stringify({
25 type: 'chat',
26 user: 'alice',
27 text: 'Hello everyone!',
28 timestamp: Date.now(),
29}));
30
31// Send binary data
32const buffer = new ArrayBuffer(8);
33const view = new DataView(buffer);
34view.setFloat64(0, 3.14159);
35ws.send(buffer);
36
37// Send ArrayBuffer or Blob
38const blob = new Blob(['file data'], { type: 'application/octet-stream' });
39ws.send(blob);

info

Always use wss:// (WebSocket Secure) in production. Most browsers block WebSocket connections on non-HTTPS pages, and mixed content policies will prevent ws:// connections from secure origins.
Protocol Details

The WebSocket protocol (RFC 6455) begins with an HTTP/1.1 upgrade handshake. The client sends an Upgrade header and the server responds with 101 Switching Protocols. After the handshake, the connection operates at the TCP level with framed messages.

ws-protocol.txt
TEXT
1// WebSocket handshake (HTTP upgrade)
2// Client request:
3GET /chat HTTP/1.1
4Host: example.com
5Upgrade: websocket
6Connection: Upgrade
7Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
8Sec-WebSocket-Version: 13
9Origin: https://example.com
10
11// Server response:
12HTTP/1.1 101 Switching Protocols
13Upgrade: websocket
14Connection: Upgrade
15Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
16
17// After handshake, data flows as frames:
18// Frame format:
19// 0 1 2 3
20// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
21// +-+-+-+-+-------+-+-------------+-------------------------------+
22// |F|R|R|R| opcode|M| Payload len | Extended payload length |
23// |I|S|S|S| (4) |A| (7) | (16/64) |
24// |N|V|V|V| |S| | (if payload len==126/127) |
25// | |1|2|3| |K| | |
26// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
27// | Extended payload length continued, if payload len == 127 |
28// + - - - - - - - - - - - - - - - +-------------------------------+
29// | |Masking-key, if MASK set to 1 |
30// +-------------------------------+-------------------------------+
31// | Masking-key (continued) | Payload Data |
32// +-------------------------------- - - - - - - - - - - - - - - - +
33// : Payload Data continued ... :
34// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+
35// | Payload Data (continued) |
36// +---------------------------------------------------------------+
37
38// Opcodes:
39// 0x0 — Continuation frame
40// 0x1 — Text frame (UTF-8)
41// 0x2 — Binary frame
42// 0x8 — Connection close
43// 0x9 — Ping
44// 0xA — Pong

best practice

The browser handles framing automatically — you never interact with raw frames in JavaScript. Understanding the protocol is useful for debugging with tools like Wireshark or when implementing a WebSocket server.
Reconnection Strategies

WebSocket connections can drop due to network issues, server restarts, or timeouts. A robust reconnection strategy is essential for production apps. Use exponential backoff with jitter to avoid thundering herd problems.

reconnecting-ws.js
JavaScript
1class ReconnectingWebSocket {
2 constructor(url, protocols) {
3 this.url = url;
4 this.protocols = protocols;
5 this.reconnectAttempts = 0;
6 this.maxReconnectAttempts = 20;
7 this.baseDelay = 1000;
8 this.maxDelay = 30000;
9 this.listeners = {};
10 this.connect();
11 }
12
13 connect() {
14 this.ws = new WebSocket(this.url, this.protocols);
15
16 this.ws.addEventListener('open', () => {
17 this.reconnectAttempts = 0;
18 this.emit('open');
19 });
20
21 this.ws.addEventListener('message', (event) => {
22 this.emit('message', event);
23 });
24
25 this.ws.addEventListener('close', (event) => {
26 this.emit('close', event);
27 if (!event.wasClean) {
28 this.scheduleReconnect();
29 }
30 });
31
32 this.ws.addEventListener('error', (event) => {
33 this.emit('error', event);
34 });
35 }
36
37 scheduleReconnect() {
38 if (this.reconnectAttempts >= this.maxReconnectAttempts) {
39 this.emit('reconnectFailed');
40 return;
41 }
42
43 // Exponential backoff with jitter
44 const delay = Math.min(
45 this.baseDelay * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000,
46 this.maxDelay
47 );
48
49 this.reconnectAttempts++;
50 console.log(
51 `Reconnecting in ${Math.round(delay)}ms `
52 + `(${this.reconnectAttempts}/${this.maxReconnectAttempts})`
53 );
54
55 this.reconnectTimer = setTimeout(() => {
56 this.connect();
57 }, delay);
58 }
59
60 send(data) {
61 if (this.ws.readyState === WebSocket.OPEN) {
62 this.ws.send(data);
63 } else {
64 console.warn('WebSocket not open, queuing message');
65 this.queue = this.queue || [];
66 this.queue.push(data);
67 }
68 }
69
70 on(event, callback) {
71 if (!this.listeners[event]) this.listeners[event] = [];
72 this.listeners[event].push(callback);
73 }
74
75 emit(event, ...args) {
76 (this.listeners[event] || []).forEach((cb) => cb(...args));
77 // Flush queued messages on reconnect
78 if (event === 'open' && this.queue?.length) {
79 this.queue.forEach((msg) => this.send(msg));
80 this.queue = [];
81 }
82 }
83
84 close() {
85 clearTimeout(this.reconnectTimer);
86 this.maxReconnectAttempts = 0;
87 this.ws.close();
88 }
89}
90
91// Usage
92const ws = new ReconnectingWebSocket('wss://api.example.com/ws');
93ws.on('message', (event) => {
94 const data = JSON.parse(event.data);
95 handleUpdate(data);
96});
97ws.on('reconnectFailed', () => {
98 showOfflineMessage();
99});

warning

Never reconnect immediately after a close — exponential backoff is critical. Without it, a server under pressure gets flooded with reconnection attempts from all clients simultaneously (thundering herd). Add random jitter to spread reconnect attempts across time.
Rooms & Channels

WebSocket rooms and channels let you organize connections into logical groups so messages are only delivered to relevant clients. This is typically implemented server-side since the browser API is a single connection.

ws-rooms.js
JavaScript
1// Server-side (Node.js with ws library)
2const WebSocket = require('ws');
3const wss = new WebSocket.Server({ port: 8080 });
4
5const rooms = new Map(); // roomId -> Set<ws>
6const clientRooms = new Map(); // ws -> Set<roomId>
7
8wss.on('connection', (ws) => {
9 clientRooms.set(ws, new Set());
10
11 ws.on('message', (raw) => {
12 const msg = JSON.parse(raw);
13
14 switch (msg.type) {
15 case 'join':
16 joinRoom(ws, msg.room);
17 break;
18 case 'leave':
19 leaveRoom(ws, msg.room);
20 break;
21 case 'message':
22 broadcastToRoom(msg.room, {
23 type: 'message',
24 room: msg.room,
25 user: msg.user,
26 text: msg.text,
27 timestamp: Date.now(),
28 }, ws);
29 break;
30 }
31 });
32
33 ws.on('close', () => {
34 const roomSet = clientRooms.get(ws);
35 if (roomSet) {
36 roomSet.forEach((roomId) => leaveRoom(ws, roomId));
37 }
38 clientRooms.delete(ws);
39 });
40});
41
42function joinRoom(ws, roomId) {
43 if (!rooms.has(roomId)) rooms.set(roomId, new Set());
44 rooms.get(roomId).add(ws);
45 clientRooms.get(ws).add(roomId);
46
47 ws.send(JSON.stringify({
48 type: 'joined',
49 room: roomId,
50 users: rooms.get(roomId).size,
51 }));
52
53 broadcastToRoom(roomId, {
54 type: 'userJoined',
55 room: roomId,
56 users: rooms.get(roomId).size,
57 }, ws);
58}
59
60function leaveRoom(ws, roomId) {
61 const room = rooms.get(roomId);
62 if (room) {
63 room.delete(ws);
64 clientRooms.get(ws)?.delete(roomId);
65 if (room.size === 0) rooms.delete(roomId);
66 }
67}
68
69function broadcastToRoom(roomId, message, exclude) {
70 const room = rooms.get(roomId);
71 if (!room) return;
72 const data = JSON.stringify(message);
73 room.forEach((client) => {
74 if (client !== exclude && client.readyState === WebSocket.OPEN) {
75 client.send(data);
76 }
77 });
78}
79
80// Client-side — join and use rooms
81const ws = new WebSocket('wss://api.example.com/ws');
82
83ws.onopen = () => {
84 ws.send(JSON.stringify({ type: 'join', room: 'general' }));
85 ws.send(JSON.stringify({ type: 'join', room: 'notifications' }));
86};
87
88function sendToRoom(room, text) {
89 ws.send(JSON.stringify({
90 type: 'message',
91 room,
92 user: currentUser,
93 text,
94 }));
95}

info

For production apps, consider using Socket.IO which provides rooms, namespaces, and automatic reconnection out of the box. For simpler needs, implement room tracking on the server with a Map of room IDs to client sets.
BroadcastChannel API

The BroadcastChannel API enables communication between different tabs, windows, and iframes of the same origin. Unlike WebSockets (which go through a server), BroadcastChannel is purely client-side and useful for synchronizing state across browser instances.

broadcast-channel.js
JavaScript
1// Create a channel (same name in all tabs)
2const channel = new BroadcastChannel('app-state');
3
4// Send messages to all other tabs
5channel.postMessage({
6 type: 'STATE_UPDATE',
7 payload: { theme: 'dark', language: 'en' },
8});
9
10channel.postMessage({
11 type: 'LOGOUT',
12 userId: 'user-123',
13});
14
15// Listen for messages from other tabs
16channel.onmessage = (event) => {
17 const { type, payload } = event.data;
18 switch (type) {
19 case 'STATE_UPDATE':
20 applyStateUpdate(payload);
21 break;
22 case 'LOGOUT':
23 if (payload.userId === currentUser.id) {
24 window.location.href = '/login';
25 }
26 break;
27 }
28};
29
30// Close when tab closes
31channel.close();
32
33// Advanced: Cross-tab event synchronization
34class CrossTabSync {
35 constructor(channelName) {
36 this.channel = new BroadcastChannel(channelName);
37 this.pendingAcks = new Map();
38 this.channel.onmessage = (e) => this.handleMessage(e.data);
39 }
40
41 broadcast(type, data) {
42 const id = crypto.randomUUID();
43 this.channel.postMessage({ type, data, id, origin: this.tabId });
44 return new Promise((resolve) => {
45 this.pendingAcks.set(id, { resolve, timestamp: Date.now() });
46 setTimeout(() => this.pendingAcks.delete(id), 5000);
47 });
48 }
49
50 handleMessage(msg) {
51 if (msg.origin === this.tabId) return;
52 if (msg.type === 'ACK' && this.pendingAcks.has(msg.id)) {
53 this.pendingAcks.get(msg.id).resolve();
54 this.pendingAcks.delete(msg.id);
55 return;
56 }
57 // Process the message
58 this.channel.postMessage({
59 type: 'ACK',
60 id: msg.id,
61 origin: this.tabId,
62 });
63 }
64
65 get tabId() {
66 return this._tabId || (this._tabId = crypto.randomUUID());
67 }
68}

best practice

BroadcastChannel is perfect for keeping multiple tabs in sync — like logging out a user from all tabs, syncing theme preferences, or preventing duplicate form submissions. For server communication, use WebSockets instead.
Authentication & Security

WebSocket connections cannot set custom headers during the initial handshake, so authentication must use query parameters, cookies, or send a token as the first message after connection. Always validate tokens server-side.

ws-auth.js
JavaScript
1// Method 1: Token in query string (less secure — visible in logs)
2const token = await getAuthToken();
3const ws = new WebSocket(
4 `wss://api.example.com/ws?token=${encodeURIComponent(token)}`
5);
6
7// Method 2: Cookie-based auth (works with SameSite cookies)
8// If the WS server is on the same origin, cookies are sent automatically
9const ws = new WebSocket('wss://api.example.com/ws');
10
11// Method 3: Send token as first message (most common)
12const ws = new WebSocket('wss://api.example.com/ws');
13
14ws.addEventListener('open', async () => {
15 const token = await getAuthToken();
16 ws.send(JSON.stringify({
17 type: 'auth',
18 token,
19 }));
20});
21
22// Server-side validation (Node.js)
23wss.on('connection', (ws, req) => {
24 let authenticated = false;
25 const authTimeout = setTimeout(() => {
26 if (!authenticated) {
27 ws.close(4001, 'Authentication timeout');
28 }
29 }, 5000);
30
31 ws.on('message', (raw) => {
32 const msg = JSON.parse(raw);
33
34 if (!authenticated) {
35 if (msg.type === 'auth') {
36 const user = verifyToken(msg.token);
37 if (user) {
38 authenticated = true;
39 clearTimeout(authTimeout);
40 ws.user = user;
41 ws.send(JSON.stringify({ type: 'authSuccess', user: user.id }));
42 } else {
43 ws.close(4003, 'Invalid token');
44 }
45 }
46 return;
47 }
48
49 // Handle authenticated messages
50 handleMessage(ws, msg);
51 });
52});
53
54// Rate limiting WebSocket messages
55const rateLimiter = new Map();
56
57function checkRateLimit(ws, maxPerSecond = 50) {
58 const userId = ws.user.id;
59 const now = Date.now();
60 const window = rateLimiter.get(userId) || { count: 0, start: now };
61
62 if (now - window.start > 1000) {
63 window.count = 0;
64 window.start = now;
65 }
66
67 window.count++;
68 rateLimiter.set(userId, window);
69
70 if (window.count > maxPerSecond) {
71 ws.send(JSON.stringify({ type: 'error', message: 'Rate limit exceeded' }));
72 return false;
73 }
74 return true;
75}

warning

Never put sensitive tokens in the WebSocket URL query string — they appear in server logs, browser history, and potentially proxy logs. Use cookies or send the token as the first message after the connection is established.
Binary Data & Performance

WebSockets support both text (UTF-8) and binary frames. For high-throughput scenarios, binary data with protocols like MessagePack or Protocol Buffers reduces serialization overhead compared to JSON.

ws-binary.js
JavaScript
1// Binary protocol with ArrayBuffer
2function sendBinaryVector(ws, x, y, z) {
3 const buffer = new ArrayBuffer(12);
4 const view = new DataView(buffer);
5 view.setFloat32(0, x);
6 view.setFloat32(4, y);
7 view.setFloat32(8, z);
8 ws.send(buffer);
9}
10
11function parseBinaryVector(data) {
12 const view = new DataView(data);
13 return {
14 x: view.getFloat32(0),
15 y: view.getFloat32(4),
16 z: view.getFloat32(8),
17 };
18}
19
20// MessagePack — compact binary JSON alternative
21// npm install @msgpack/msgpack
22import { encode, decode } from '@msgpack/msgpack';
23
24// Encode and send
25const message = { type: 'position', x: 1.5, y: 2.3, z: -0.7 };
26ws.send(encode(message));
27
28// Receive and decode
29ws.binaryType = 'arraybuffer';
30ws.onmessage = (event) => {
31 const data = decode(new Uint8Array(event.data));
32 console.log(data); // { type: 'position', x: 1.5, y: 2.3, z: -0.7 }
33};
34
35// Compression for large payloads
36async function sendCompressed(ws, data) {
37 const json = JSON.stringify(data);
38 const stream = new Blob([json]).stream();
39 const compressed = stream.pipeThrough(
40 new CompressionStream('gzip')
41 );
42 const buffer = await new Response(compressed).arrayBuffer();
43 ws.send(buffer);
44}
45
46// Throttle high-frequency updates
47function createThrottledSender(ws, maxHz = 60) {
48 let lastSend = 0;
49 let pendingData = null;
50
51 return function send(data) {
52 pendingData = data;
53 const now = performance.now();
54 const interval = 1000 / maxHz;
55
56 if (now - lastSend >= interval) {
57 lastSend = now;
58 ws.send(typeof data === 'string' ? data : JSON.stringify(data));
59 pendingData = null;
60 } else if (!pendingData?._scheduled) {
61 pendingData._scheduled = true;
62 setTimeout(() => {
63 if (pendingData) {
64 lastSend = performance.now();
65 ws.send(
66 typeof pendingData === 'string'
67 ? pendingData
68 : JSON.stringify(pendingData)
69 );
70 pendingData = null;
71 }
72 }, interval - (now - lastSend));
73 }
74 };
75}
🔥

pro tip

For real-time games or VR applications where every millisecond matters, use binary protocols and avoid JSON. MessagePack gives you 30-50% smaller payloads than JSON with faster serialization. For structured binary data, consider FlatBuffers.
Server Libraries

While the browser provides a simple WebSocket API, server-side libraries add critical features like rooms, namespaces, middleware, automatic reconnection, and transport fallbacks. Here are the most popular options.

ws-server-options.js
JavaScript
1// ws — Lightweight, no-frills (Node.js)
2const WebSocket = require('ws');
3const wss = new WebSocket.Server({ port: 8080 });
4wss.on('connection', (ws) => {
5 ws.on('message', (data) => ws.send(data)); // echo
6});
7
8// Socket.IO — Full-featured with fallbacks
9const { Server } = require('socket.io');
10const io = new Server(3000, {
11 cors: { origin: 'https://example.com' },
12});
13
14io.on('connection', (socket) => {
15 socket.join('general');
16 socket.to('general').emit('message', 'User joined');
17 socket.on('chat', (msg) => io.to('general').emit('chat', msg));
18});
19
20// µWebSockets.js — High performance (10x faster than ws)
21const uWS = require('uWebSockets.js');
22uWS.App()
23 .ws('/*', {
24 message: (ws, message) => {
25 ws.send(message);
26 },
27 })
28 .listen(9001);
29
30// Client-side with Socket.IO
31import { io } from 'socket.io-client';
32const socket = io('https://api.example.com', {
33 auth: { token: userToken },
34 reconnection: true,
35 reconnectionDelay: 1000,
36 reconnectionDelayMax: 5000,
37 reconnectionAttempts: 20,
38});
39
40socket.on('connect', () => console.log('Connected'));
41socket.on('message', (data) => handleMessage(data));
42socket.emit('join', 'chat-room');
43
44// ably/pubnub — Managed WebSocket services
45// Provides pub/sub, presence, history, and connection management
46// without running your own WebSocket server

best practice

For most apps, Socket.IO provides the best developer experience with built-in reconnection, rooms, and HTTP long-polling fallback. For maximum performance, use uWebSockets.js. For managed infrastructure, consider Ably or Pusher.
Key Takeaways
  • WebSockets provide full-duplex communication over a single TCP connection
  • Always use wss:// for secure connections in production
  • Implement exponential backoff with jitter for reliable reconnection
  • Rooms and channels organize messages to relevant clients
  • Use BroadcastChannel for cross-tab communication without a server
  • Authenticate via cookies or first message — never in query strings
  • Binary protocols (MessagePack, Protobuf) outperform JSON for high-throughput