JavaScript — WebSockets
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).
| 1 | // Basic WebSocket connection |
| 2 | const socket = new WebSocket('wss://example.com/ws'); |
| 3 | |
| 4 | // Connection opened |
| 5 | socket.addEventListener('open', (event) => { |
| 6 | console.log('Connected to server'); |
| 7 | socket.send('Hello Server!'); |
| 8 | }); |
| 9 | |
| 10 | // Listen for messages |
| 11 | socket.addEventListener('message', (event) => { |
| 12 | console.log('Message from server:', event.data); |
| 13 | }); |
| 14 | |
| 15 | // Listen for errors |
| 16 | socket.addEventListener('error', (event) => { |
| 17 | console.error('WebSocket error:', event); |
| 18 | }); |
| 19 | |
| 20 | // Listen for connection close |
| 21 | socket.addEventListener('close', (event) => { |
| 22 | console.log('Disconnected:', event.code, event.reason); |
| 23 | }); |
| 24 | |
| 25 | // Close the connection |
| 26 | socket.close(1000, 'Client closing'); // 1000 = Normal Closure |
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.
| State | Constant | Value | Description |
|---|---|---|---|
| CONNECTING | WebSocket.CONNECTING | 0 | Handshake in progress, not yet open |
| OPEN | WebSocket.OPEN | 1 | Connection established, ready to send/receive |
| CLOSING | WebSocket.CLOSING | 2 | Close handshake in progress |
| CLOSED | WebSocket.CLOSED | 3 | Connection closed or failed to open |
| 1 | // Connection lifecycle management |
| 2 | class 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 | } |
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.
| 1 | // Sending different data types |
| 2 | const socket = new WebSocket('wss://example.com/ws'); |
| 3 | |
| 4 | // Text message (most common) |
| 5 | socket.send('Hello, server!'); |
| 6 | socket.send(JSON.stringify({ type: 'greeting', payload: { name: 'John' } })); |
| 7 | |
| 8 | // Binary data |
| 9 | const buffer = new ArrayBuffer(8); |
| 10 | const view = new DataView(buffer); |
| 11 | view.setInt32(0, 12345); |
| 12 | view.setFloat32(4, 3.14); |
| 13 | socket.send(buffer); |
| 14 | |
| 15 | // Blob |
| 16 | const blob = new Blob(['binary data'], { type: 'application/octet-stream' }); |
| 17 | socket.send(blob); |
| 18 | |
| 19 | // Receiving messages |
| 20 | socket.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 |
| 42 | socket.binaryType = 'arraybuffer'; // or 'blob' (default) |
| 43 | // Setting this determines how binary data is received |
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 Type | JavaScript Type (receive) | send() Argument | Use Case |
|---|---|---|---|
| Text | string | string | JSON, XML, plain text messages |
| Binary | Blob or ArrayBuffer | ArrayBuffer, TypedArray, Blob | Files, images, protocol buffers, audio |
| 1 | // Configuring binary type |
| 2 | const socket = new WebSocket('wss://example.com/ws'); |
| 3 | |
| 4 | // 'blob' is the default — provides a Blob object |
| 5 | socket.binaryType = 'blob'; |
| 6 | // 'arraybuffer' provides an ArrayBuffer |
| 7 | socket.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 |
| 14 | socket.binaryType = 'arraybuffer'; |
| 15 | |
| 16 | socket.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 |
| 39 | function 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 | } |
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.
| 1 | // Robust reconnection with exponential backoff |
| 2 | class 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 |
| 85 | const 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
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.
| 1 | // Heartbeat to keep connection alive |
| 2 | class 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 |
| 82 | socket.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 | }); |
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.
| 1 | // Authentication strategies |
| 2 | |
| 3 | // 1. Token in URL query parameter (most common) |
| 4 | const token = await getAuthToken(); |
| 5 | const 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 |
| 10 | const 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) |
| 16 | const socket = new WebSocket('wss://example.com/ws'); |
| 17 | |
| 18 | socket.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 | |
| 27 | socket.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). |
| 52 | const socket = new WebSocket('wss://example.com/ws'); |
| 53 | // Browser automatically includes cookies for the domain |
warning
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.
| Feature | WebSocket | SSE (EventSource) | Polling |
|---|---|---|---|
| Direction | Full-duplex | Server → Client only | Client → Server (pull) |
| Protocol | ws:// / wss:// | HTTP (standard) | HTTP (standard) |
| Message Format | Text or Binary | Text only (UTF-8) | Any (HTTP response) |
| Auto-reconnect | ✗ (manual) | ✓ (built-in) | ✗ (manual) |
| Browser Support | All modern | All modern (except IE) | Universal |
| Complexity | Higher | Lower | Simple |
| Latency | Low (real-time) | Low (real-time) | Depends on interval |
| Best For | Chat, gaming, collaborative editing | Live feeds, notifications, stock tickers | Simple updates, legacy systems, low frequency |
| 1 | // Server-Sent Events (SSE) — one-way push |
| 2 | const eventSource = new EventSource('/api/events'); |
| 3 | |
| 4 | eventSource.addEventListener('open', () => { |
| 5 | console.log('SSE connection opened'); |
| 6 | }); |
| 7 | |
| 8 | eventSource.addEventListener('message', (event) => { |
| 9 | const data = JSON.parse(event.data); |
| 10 | console.log('SSE message:', data); |
| 11 | }); |
| 12 | |
| 13 | eventSource.addEventListener('error', (event) => { |
| 14 | console.error('SSE error:', event); |
| 15 | }); |
| 16 | |
| 17 | // Named events |
| 18 | eventSource.addEventListener('user-update', (event) => { |
| 19 | console.log('User update:', JSON.parse(event.data)); |
| 20 | }); |
| 21 | |
| 22 | eventSource.close(); // stop listening |
| 23 | |
| 24 | // Polling approach |
| 25 | async 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 |
| 41 | const pollId = await startPolling(3000); |
| 42 | clearInterval(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
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.
| 1 | // Buffered WebSocket with automatic flush |
| 2 | class 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 |
| 59 | class 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 |
| 86 | const ws = new BufferedWebSocket('wss://example.com/ws'); |
| 87 | |
| 88 | // These will be buffered if connection isn't open yet |
| 89 | ws.send(JSON.stringify({ type: 'join', room: 'general' })); |
| 90 | ws.send(JSON.stringify({ type: 'message', text: 'Hello!' })); |
| 91 | |
| 92 | // Once connection opens, buffer is flushed automatically |
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.
| 1 | // Chat client class |
| 2 | class 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 |
| 116 | const chat = new ChatClient('wss://chat.example.com/ws', 'John'); |
| 117 | |
| 118 | chat.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 | |
| 138 | chat.connect(); |
| 139 | chat.joinRoom('general'); |
| 140 | chat.sendMessage('general', 'Hello everyone!'); |
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.
| 1 | // Real-time data feed client |
| 2 | class 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 |
| 79 | const feed = new DataFeed('wss://market.example.com/feed'); |
| 80 | |
| 81 | // Subscribe to stock prices |
| 82 | const unsubAAPL = feed.subscribe('stock.AAPL', (price) => { |
| 83 | updatePrice('AAPL', price); |
| 84 | }); |
| 85 | |
| 86 | const unsubGOOG = feed.subscribe('stock.GOOG', (price) => { |
| 87 | updatePrice('GOOG', price); |
| 88 | }); |
| 89 | |
| 90 | // Subscribe to news alerts |
| 91 | const unsubNews = feed.subscribe('news.breaking', (article) => { |
| 92 | showNotification(article); |
| 93 | }); |
| 94 | |
| 95 | // Unsubscribe when no longer needed |
| 96 | unsubAAPL(); |
| 97 | |
| 98 | // Batch subscribe for efficiency |
| 99 | const tickers = ['AAPL', 'GOOG', 'MSFT', 'AMZN', 'TSLA']; |
| 100 | tickers.forEach(ticker => { |
| 101 | feed.subscribe(`stock.${ticker}`, (price) => { |
| 102 | updatePrice(ticker, price); |
| 103 | }); |
| 104 | }); |
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).
| 1 | // Security best practices |
| 2 | const 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) |
| 12 | function sanitizeMessage(text) { |
| 13 | return text |
| 14 | .replace(/</g, '<') |
| 15 | .replace(/>/g, '>') |
| 16 | .replace(/"/g, '"') |
| 17 | .substring(0, 2000); // limit length |
| 18 | } |
| 19 | |
| 20 | // Handling close codes |
| 21 | socket.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) |
| 38 | class 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