WebSockets Interview Questions (Free Preview)
Free sample of 15 from 31 questions available
WebSocket Fundamentals
What is the difference between WebSocket and Server-Sent Events (SSE)?
The 30-Second Answer: WebSocket provides full-duplex bidirectional communication over a custom protocol, while Server-Sent Events (SSE) is a unidirectional HTTP-based technology where the server pushes updates to the client. SSE is simpler, works over regular HTTP/2, and auto-reconnects, but only supports server-to-client messaging, whereas WebSocket requires both parties to send data.
The 2-Minute Answer (If They Want More): SSE (EventSource API) is built on top of standard HTTP using text/event-stream responses. The client makes a regular HTTP request, and the server keeps the connection open, sending updates as formatted text events. SSE automatically handles reconnection with Last-Event-ID, integrates seamlessly with HTTP infrastructure (proxies, load balancers), and benefits from HTTP/2 multiplexing. However, it's strictly one-way: server to client only.
WebSocket establishes a custom protocol after an HTTP upgrade handshake. It supports true bidirectional communication where both client and server can send messages independently. WebSocket can transmit binary data efficiently, has lower per-message overhead for high-frequency updates, and provides more control over the connection lifecycle. However, it requires more complex infrastructure support and doesn't auto-reconnect—developers must implement reconnection logic.
For browser compatibility, SSE has one significant limitation: most browsers impose a maximum of 6 concurrent SSE connections per domain (same as HTTP connections), though HTTP/2 mitigates this. WebSocket connections don't share this limit. SSE connections are also subject to browser connection limits and may be affected by corporate proxies that buffer HTTP responses.
Choose SSE when you need server-to-client updates only (stock tickers, news feeds, notifications, progress updates), want simpler implementation, need automatic reconnection, or require HTTP/2 multiplexing. Choose WebSocket when you need bidirectional communication (chat, gaming, collaborative editing), require low latency with high message frequency, need binary data support, or want a single protocol for all real-time needs.
Code Example:
// Server-Sent Events (SSE) - Server to Client only
class SSEClient {
constructor(url) {
this.eventSource = new EventSource(url);
// Receive server updates
this.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Server update:', data);
};
// Custom event types
this.eventSource.addEventListener('price-update', (event) => {
console.log('Stock price:', JSON.parse(event.data));
});
// Automatic reconnection on connection loss
this.eventSource.onerror = (error) => {
console.log('Connection error, auto-reconnecting...');
// Browser automatically attempts to reconnect
};
}
// CANNOT send data back to server via SSE
// Must use separate HTTP POST/PUT requests
async sendFeedback(message) {
await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
}
}
// WebSocket - Bidirectional communication
class WebSocketClient {
constructor(url) {
this.connect(url);
}
connect(url) {
this.ws = new WebSocket(url);
this.ws.onopen = () => {
console.log('Connected');
// Can send immediately
this.send({ type: 'subscribe', channel: 'stocks' });
};
// Receive messages from server
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
// Manual reconnection logic required
this.ws.onclose = () => {
console.log('Disconnected, reconnecting in 3s...');
setTimeout(() => this.connect(url), 3000);
};
}
// Can send data anytime over the same connection
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
}
// Server-side comparison (Node.js)
const http = require('http');
// SSE Server
function createSSEServer() {
const server = http.createServer((req, res) => {
if (req.url === '/events') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// Send updates periodically
const interval = setInterval(() => {
res.write(`data: ${JSON.stringify({
price: Math.random() * 100,
timestamp: Date.now()
})}\n\n`);
}, 1000);
req.on('close', () => clearInterval(interval));
}
});
return server;
}
// WebSocket requires dedicated library (ws, socket.io, etc.)
const WebSocket = require('ws');
function createWebSocketServer() {
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
// Receive messages from client
ws.on('message', (data) => {
console.log('Client sent:', data.toString());
// Can respond to specific client messages
ws.send(JSON.stringify({ type: 'ack', received: true }));
});
// Send updates
const interval = setInterval(() => {
ws.send(JSON.stringify({
price: Math.random() * 100,
timestamp: Date.now()
}));
}, 1000);
ws.on('close', () => clearInterval(interval));
});
}
Mermaid Diagram (if helpful):
graph LR
subgraph "SSE - Unidirectional"
C1[Client] -->|HTTP Request| S1[Server]
S1 -.->|Event Stream| C1
S1 -.->|Event| C1
S1 -.->|Event| C1
C1 -->|Separate HTTP POST| S1
Note1[Server pushes updates<br/>Client uses HTTP for requests<br/>Auto-reconnects]
end
subgraph "WebSocket - Bidirectional"
C2[Client] <-->|Upgrade Handshake| S2[Server]
C2 <-.->|Message| S2
C2 <-.->|Message| S2
S2 <-.->|Message| C2
Note2[Full-duplex communication<br/>Same connection for both<br/>Manual reconnect needed]
end
References:
↑ Back to topWhat is WebSocket and how does it differ from HTTP?
The 30-Second Answer: WebSocket is a protocol that provides full-duplex, bidirectional communication between client and server over a single, persistent TCP connection. Unlike HTTP's request-response model where the client must initiate every exchange, WebSocket allows both parties to send messages independently at any time after the initial handshake.
The 2-Minute Answer (If They Want More): WebSocket was standardized in 2011 (RFC 6455) to address the limitations of HTTP for real-time applications. While HTTP follows a strict request-response cycle where the client asks and the server answers, WebSocket establishes a persistent connection that remains open, allowing both client and server to push data whenever needed.
The key differences lie in the communication model and overhead. HTTP creates a new connection for each request-response cycle (or reuses connections with keep-alive, but still follows request-response), includes headers with every exchange, and operates in a half-duplex manner. WebSocket, after an initial HTTP upgrade handshake, maintains a single connection with minimal framing overhead and enables true full-duplex communication.
This makes WebSocket ideal for real-time applications like chat systems, live sports updates, collaborative editing tools, and gaming, where low latency and server-initiated updates are crucial. HTTP remains better suited for traditional request-response scenarios like fetching web pages, RESTful APIs, and scenarios where caching is beneficial.
The WebSocket protocol starts with an HTTP upgrade request, making it firewall and proxy-friendly while leveraging existing web infrastructure. Once upgraded, the connection operates at the TCP level with its own framing protocol.
Code Example:
// HTTP - Client must request data repeatedly
async function httpPolling() {
setInterval(async () => {
const response = await fetch('/api/messages');
const data = await response.json();
console.log('Received:', data);
}, 1000); // Poll every second
}
// WebSocket - Persistent bidirectional connection
const ws = new WebSocket('ws://localhost:8080');
// Server can push data anytime
ws.onmessage = (event) => {
console.log('Received:', event.data);
};
// Client can send data anytime
ws.onopen = () => {
ws.send('Hello Server!');
};
// Connection stays open until explicitly closed
ws.onclose = (event) => {
console.log('Connection closed:', event.code, event.reason);
};
Mermaid Diagram (if helpful):
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: HTTP Request-Response
C->>S: GET /api/data (Request)
S->>C: 200 OK + Data (Response)
Note over C,S: Connection typically closes
C->>S: GET /api/data (New Request)
S->>C: 200 OK + Data (Response)
Note over C,S: WebSocket Full-Duplex
C->>S: HTTP Upgrade Request
S->>C: 101 Switching Protocols
Note over C,S: Connection stays open
S->>C: Message (Server-initiated)
C->>S: Message (Client-initiated)
S->>C: Message
C->>S: Message
References:
↑ Back to topWhat is the WebSocket handshake process?
The 30-Second Answer:
The WebSocket handshake is an HTTP Upgrade request initiated by the client, containing a special Sec-WebSocket-Key header. The server responds with HTTP 101 Switching Protocols and a computed Sec-WebSocket-Accept hash, after which both parties switch from HTTP to the WebSocket protocol over the same TCP connection.
The 2-Minute Answer (If They Want More):
The handshake process begins when the client sends a standard HTTP GET request with specific upgrade headers. The request includes Connection: Upgrade, Upgrade: websocket, a protocol version number, and a randomly generated Base64-encoded Sec-WebSocket-Key. This key serves as a nonce to prevent cross-protocol attacks and confirm that both parties understand the WebSocket protocol.
Upon receiving the handshake request, the server validates the upgrade headers and computes the Sec-WebSocket-Accept value by concatenating the client's Sec-WebSocket-Key with a magic string (258EAFA5-E914-47DA-95CA-C5AB0DC85B11), then creating a SHA-1 hash and Base64-encoding the result. This computation proves the server understands WebSocket and prevents simple HTTP servers from accidentally accepting WebSocket connections.
If the server accepts the upgrade, it responds with HTTP status 101 Switching Protocols, including the computed Sec-WebSocket-Accept header and confirming the upgrade. The client validates the accept value matches its expected computation. Once this handshake completes successfully, the HTTP connection is "upgraded" to a WebSocket connection, and both parties can begin exchanging WebSocket frames.
The handshake also supports optional subprotocol negotiation via Sec-WebSocket-Protocol headers and extensions via Sec-WebSocket-Extensions, allowing clients and servers to agree on higher-level protocols and capabilities.
Code Example:
// Client-side handshake (automatic with WebSocket API)
const ws = new WebSocket('ws://example.com/socket', ['chat', 'superchat']);
ws.onopen = () => {
console.log('Handshake successful, connection open');
};
// Server-side handshake (Node.js example)
const http = require('http');
const crypto = require('crypto');
const server = http.createServer();
server.on('upgrade', (req, socket, head) => {
// Validate handshake headers
if (req.headers['upgrade'] !== 'websocket') {
socket.end('HTTP/1.1 400 Bad Request');
return;
}
const key = req.headers['sec-websocket-key'];
const MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
// Compute accept hash
const acceptKey = crypto
.createHash('sha1')
.update(key + MAGIC_STRING)
.digest('base64');
// Send upgrade response
const responseHeaders = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${acceptKey}`,
'',
''
];
socket.write(responseHeaders.join('\r\n'));
// Connection is now upgraded to WebSocket
console.log('WebSocket handshake complete');
});
server.listen(8080);
Mermaid Diagram (if helpful):
sequenceDiagram
participant C as Client
participant S as Server
C->>S: GET /socket HTTP/1.1<br/>Upgrade: websocket<br/>Connection: Upgrade<br/>Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==<br/>Sec-WebSocket-Version: 13
Note over S: Validates headers<br/>Computes SHA-1(key + magic)<br/>Base64 encodes result
S->>C: HTTP/1.1 101 Switching Protocols<br/>Upgrade: websocket<br/>Connection: Upgrade<br/>Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Note over C: Validates accept value
Note over C,S: Connection upgraded to WebSocket protocol<br/>Can now exchange WebSocket frames
References:
↑ Back to topWhat are the advantages of WebSocket over HTTP polling?
The 30-Second Answer: WebSocket eliminates HTTP polling's overhead by maintaining a persistent connection, reducing latency from seconds to milliseconds, cutting bandwidth usage by 90%+ (no repeated headers), and enabling true server-push capabilities. This results in lower server load, reduced network traffic, and real-time bidirectional communication instead of simulated real-time through constant requests.
The 2-Minute Answer (If They Want More): HTTP polling forces clients to repeatedly ask "any updates?" at fixed intervals, creating significant inefficiencies. Each poll requires a complete HTTP request-response cycle with headers (typically 500-2000 bytes), even when there's no new data. Short polling intervals waste bandwidth and server resources, while long intervals increase latency. Long polling improves latency but still requires reconnecting after each response and maintains open connections that consume server resources while waiting.
WebSocket solves these problems with a single persistent connection. After the initial handshake, the connection stays open with minimal overhead—WebSocket frames add just 2-14 bytes compared to HTTP's hundreds of bytes per message. This dramatic reduction in overhead means a WebSocket connection can transmit hundreds of small messages using less bandwidth than a few HTTP polls.
The latency improvement is equally significant. HTTP polling is limited by the polling interval (1-30 seconds typically), meaning updates can be delayed significantly. Long polling reduces this but still incurs connection setup overhead. WebSocket delivers messages instantly as they occur, achieving sub-100ms latency for most applications.
Server resource utilization also improves dramatically. While HTTP polling creates constant CPU and I/O load from processing endless requests (even with no data to send), WebSocket connections remain idle until data needs transmission, allowing a single server to handle tens of thousands of concurrent connections efficiently. This makes WebSocket ideal for applications requiring real-time updates like chat, notifications, live dashboards, multiplayer games, and collaborative tools.
Code Example:
// HTTP Short Polling - Inefficient
class HTTPPollingClient {
constructor(url, interval = 1000) {
this.url = url;
this.interval = interval;
this.pollCount = 0;
}
start() {
this.timer = setInterval(async () => {
this.pollCount++;
try {
// Each request includes full HTTP headers (500-2000 bytes)
const response = await fetch(this.url);
const data = await response.json();
if (data.messages && data.messages.length > 0) {
console.log('New messages:', data.messages);
}
// Often receives empty responses, wasting bandwidth
} catch (error) {
console.error('Poll failed:', error);
}
}, this.interval);
}
// After 1 hour with 1s polling: 3,600 requests sent
// With 800 bytes average per request/response: ~2.88 MB overhead
}
// WebSocket - Efficient real-time communication
class WebSocketClient {
constructor(url) {
this.url = url;
this.messageCount = 0;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected - single handshake only');
};
this.ws.onmessage = (event) => {
this.messageCount++;
// Messages arrive instantly with ~2-14 bytes frame overhead
const data = JSON.parse(event.data);
console.log('Real-time message:', data);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Connection stays open until explicitly closed
}
// After 1 hour with 100 messages received:
// Single handshake + 100 messages with ~10 bytes frame overhead each
// Total: ~2 KB handshake + ~1 KB frames = ~3 KB overhead
// Savings: 99.9% compared to polling
}
// Comparison
const httpClient = new HTTPPollingClient('/api/messages');
httpClient.start(); // Creates 3,600 requests/hour
const wsClient = new WebSocketClient('ws://localhost:8080');
wsClient.connect(); // Single persistent connection
Mermaid Diagram (if helpful):
graph TD
subgraph "HTTP Polling - 10 updates/minute"
A1[Client] -->|Request + Headers 800B| B1[Server]
B1 -->|Response + Headers 800B| A1
A1 -->|Request 800B| B1
B1 -->|Empty Response 800B| A1
A1 -->|Request 800B| B1
B1 -->|Empty Response 800B| A1
Note1[10 updates = 60 polls/min<br/>~96KB overhead<br/>Average latency: 3s]
end
subgraph "WebSocket - 10 updates/minute"
A2[Client] <-->|Persistent Connection| B2[Server]
B2 -.->|Message + 10B frame| A2
B2 -.->|Message + 10B frame| A2
B2 -.->|Message + 10B frame| A2
Note2[10 updates = 10 frames<br/>~100B overhead<br/>Latency: <100ms]
end
References:
↑ Back to topMessage Handling
What is the difference between text and binary WebSocket messages?
The 30-Second Answer: Text messages are UTF-8 encoded strings transmitted as text frames (opcode 0x1), while binary messages are raw byte data transmitted as binary frames (opcode 0x2). Text messages are automatically validated for valid UTF-8 encoding, whereas binary messages can contain any byte sequence without validation.
The 2-Minute Answer (If They Want More): At the protocol level, WebSocket distinguishes between text and binary messages using different frame opcodes. Text frames (opcode 0x1) must contain valid UTF-8 encoded data, and the WebSocket implementation will validate this encoding. If invalid UTF-8 is detected in a text frame, the connection will be closed with an error. This makes text frames ideal for JSON, XML, or plain text communication where character encoding matters.
Binary frames (opcode 0x2) carry raw byte data without any encoding constraints or validation. This makes them perfect for transmitting images, audio, video, protocol buffers, MessagePack, or any custom binary protocol. Binary data is more efficient for structured data because it avoids the overhead of string encoding and can represent data in its native format.
In JavaScript, when I send data using ws.send(), strings are automatically sent as text frames, while ArrayBuffer, Blob, and TypedArray objects are sent as binary frames. When receiving data, I can control how binary messages are delivered by setting the binaryType property to either "blob" or "arraybuffer". Text messages always arrive as JavaScript strings.
The choice between text and binary affects performance, bandwidth usage, and compatibility. Text messages are human-readable and easier to debug, but binary messages are more compact and efficient for structured data. For example, sending a JSON object as text requires string serialization and parsing, while a binary format like MessagePack can represent the same data with less overhead and no parsing ambiguity.
Code Example:
const ws = new WebSocket('wss://example.com/socket');
// Configure binary message format
ws.binaryType = 'arraybuffer'; // Options: 'blob' or 'arraybuffer'
ws.addEventListener('open', () => {
// TEXT MESSAGES
// Send plain text (text frame, opcode 0x1)
ws.send('Hello, World!');
// Send JSON as text
const jsonData = { type: 'update', value: 42 };
ws.send(JSON.stringify(jsonData));
// BINARY MESSAGES
// Send ArrayBuffer (binary frame, opcode 0x2)
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setInt32(0, 12345678, true); // little-endian
view.setFloat32(4, 3.14159, true);
ws.send(buffer);
// Send Uint8Array (binary frame)
const bytes = new Uint8Array([0xFF, 0x00, 0xAA, 0x55]);
ws.send(bytes);
// Send Blob (binary frame)
const blob = new Blob(['Binary content'], { type: 'application/octet-stream' });
ws.send(blob);
// Send image data (binary)
fetch('/image.png')
.then(response => response.blob())
.then(imageBlob => ws.send(imageBlob));
});
ws.addEventListener('message', (event) => {
// Detect message type
if (typeof event.data === 'string') {
// TEXT MESSAGE RECEIVED
console.log('Text message:', event.data);
// Try parsing as JSON
try {
const parsed = JSON.parse(event.data);
console.log('Parsed JSON:', parsed);
} catch (e) {
console.log('Plain text (not JSON)');
}
} else if (event.data instanceof ArrayBuffer) {
// BINARY MESSAGE RECEIVED (binaryType = 'arraybuffer')
console.log('Binary message (ArrayBuffer):', event.data.byteLength, 'bytes');
// Read structured binary data
const view = new DataView(event.data);
const messageType = view.getUint8(0);
const payload = view.getInt32(1, true);
console.log('Message type:', messageType, 'Payload:', payload);
} else if (event.data instanceof Blob) {
// BINARY MESSAGE RECEIVED (binaryType = 'blob')
console.log('Binary message (Blob):', event.data.size, 'bytes');
// Convert Blob to ArrayBuffer for processing
event.data.arrayBuffer().then(buffer => {
const view = new Uint8Array(buffer);
console.log('Blob data:', view);
});
// Or read as text if needed
event.data.text().then(text => {
console.log('Blob as text:', text);
});
}
});
// Example: Efficient binary protocol
class BinaryProtocol {
// Encode message to binary
static encode(type, data) {
const encoder = new TextEncoder();
const dataBytes = encoder.encode(JSON.stringify(data));
// 1 byte for type + 4 bytes for length + data
const buffer = new ArrayBuffer(5 + dataBytes.length);
const view = new DataView(buffer);
view.setUint8(0, type); // Message type
view.setUint32(1, dataBytes.length, true); // Data length
const payload = new Uint8Array(buffer, 5);
payload.set(dataBytes);
return buffer;
}
// Decode binary message
static decode(buffer) {
const view = new DataView(buffer);
const type = view.getUint8(0);
const length = view.getUint32(1, true);
const decoder = new TextDecoder();
const dataBytes = new Uint8Array(buffer, 5, length);
const data = JSON.parse(decoder.decode(dataBytes));
return { type, data };
}
}
// Usage
const message = BinaryProtocol.encode(1, { user: 'John', action: 'login' });
ws.send(message); // Sent as binary frame
// Receiving
ws.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
const { type, data } = BinaryProtocol.decode(event.data);
console.log('Message type:', type, 'Data:', data);
}
});
Mermaid Diagram (if helpful):
flowchart TD
A[Send Data via ws.send] --> B{Data Type?}
B -->|String| C[Text Frame\nOpcode 0x1\nUTF-8 Validated]
B -->|ArrayBuffer| D[Binary Frame\nOpcode 0x2\nRaw Bytes]
B -->|Blob| D
B -->|TypedArray| D
C --> E[Network Transmission]
D --> E
E --> F[Server/Client Receives]
F --> G{Frame Type?}
G -->|Text Frame| H[String\nevent.data is string]
G -->|Binary Frame| I{binaryType?}
I -->|'arraybuffer'| J[ArrayBuffer\nevent.data instanceof ArrayBuffer]
I -->|'blob'| K[Blob\nevent.data instanceof Blob]
References:
↑ Back to topHow do you send and receive messages over WebSocket?
The 30-Second Answer:
I send messages using the send() method on the WebSocket instance, which accepts strings, ArrayBuffers, Blobs, or TypedArrays. I receive messages by listening to the message event, which fires whenever the server sends data to the client.
The 2-Minute Answer (If They Want More):
The WebSocket API provides a straightforward message exchange mechanism. To send messages, I call ws.send(data) on an open WebSocket connection. The data can be a string (for text messages), ArrayBuffer, Blob, or any TypedArray (for binary messages). The connection must be in the OPEN state (readyState === 1) before sending, otherwise the send will fail.
For receiving messages, I attach an event listener to the message event. The event object contains a data property with the received payload. The type of data depends on what the server sent - it could be a string for text frames or a Blob/ArrayBuffer for binary frames. I can control how binary data is delivered by setting the binaryType property on the WebSocket instance to either "blob" (default) or "arraybuffer".
It's important to handle the asynchronous nature of WebSocket communication. Messages can arrive at any time, so my event handlers need to be set up before the connection opens. I also need to handle potential errors and connection closures gracefully, ensuring that my application doesn't try to send messages on a closed connection.
For production applications, I typically implement a message queue that buffers outgoing messages until the connection is open, and I add retry logic for critical messages. I also implement proper error handling and connection state management to ensure reliable message delivery.
Code Example:
// Create WebSocket connection
const ws = new WebSocket('wss://example.com/socket');
// Set binary data type (before connection opens)
ws.binaryType = 'arraybuffer'; // or 'blob'
// Wait for connection to open
ws.addEventListener('open', (event) => {
console.log('Connected to server');
// Send text message
ws.send('Hello Server!');
// Send JSON data
ws.send(JSON.stringify({ type: 'greeting', message: 'Hello' }));
// Send binary data (ArrayBuffer)
const buffer = new Uint8Array([1, 2, 3, 4, 5]);
ws.send(buffer);
});
// Receive messages
ws.addEventListener('message', (event) => {
// Check if data is text or binary
if (typeof event.data === 'string') {
console.log('Text message received:', event.data);
// Parse JSON if expected
try {
const message = JSON.parse(event.data);
console.log('Parsed message:', message);
} catch (e) {
console.log('Plain text:', event.data);
}
} else if (event.data instanceof ArrayBuffer) {
// Handle binary data as ArrayBuffer
const view = new Uint8Array(event.data);
console.log('Binary message received:', view);
} else if (event.data instanceof Blob) {
// Handle binary data as Blob
event.data.arrayBuffer().then(buffer => {
const view = new Uint8Array(buffer);
console.log('Binary message (from Blob):', view);
});
}
});
// Safe send function with connection check
function safeSend(ws, data) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data);
return true;
} else {
console.warn('WebSocket not open. Current state:', ws.readyState);
return false;
}
}
// Message queue for buffering messages
class WebSocketClient {
constructor(url) {
this.ws = new WebSocket(url);
this.messageQueue = [];
this.ws.addEventListener('open', () => {
// Send queued messages
while (this.messageQueue.length > 0) {
this.ws.send(this.messageQueue.shift());
}
});
this.ws.addEventListener('message', (event) => {
this.handleMessage(event.data);
});
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
// Queue message until connection opens
this.messageQueue.push(data);
}
}
handleMessage(data) {
// Custom message handling logic
console.log('Message received:', data);
}
}
Mermaid Diagram (if helpful):
sequenceDiagram
participant Client
participant WebSocket
participant Server
Client->>WebSocket: new WebSocket(url)
WebSocket->>Server: Connection Request
Server-->>WebSocket: Connection Accepted
WebSocket-->>Client: 'open' event
Client->>WebSocket: send("Hello")
WebSocket->>Server: Text Frame
Server->>WebSocket: Text Frame
WebSocket->>Client: 'message' event
Client->>WebSocket: send(ArrayBuffer)
WebSocket->>Server: Binary Frame
Server->>WebSocket: Binary Frame
WebSocket->>Client: 'message' event
References:
↑ Back to topSecurity
What is the difference between ws:// and wss://?
The 30-Second Answer: The difference between ws:// and wss:// is encryption: ws:// is unencrypted WebSocket communication (like HTTP), while wss:// is encrypted using TLS/SSL (like HTTPS). In production, you should always use wss:// to prevent eavesdropping, man-in-the-middle attacks, and data tampering.
The 2-Minute Answer (If They Want More): The ws:// and wss:// protocols differ in the same way HTTP and HTTPS differ. WebSocket Secure (wss://) wraps the WebSocket connection in a TLS/SSL layer, providing encryption, authentication, and data integrity. This encryption happens at the transport layer, securing all data transmitted between client and server before it leaves the network interface.
Using wss:// prevents several critical security vulnerabilities. Without encryption, attackers can intercept and read all WebSocket messages in plain text, modify messages in transit without detection, hijack connections by stealing session identifiers, and inject malicious data into the communication stream. Network administrators, ISPs, or anyone with access to the network path can potentially monitor unencrypted ws:// traffic.
Beyond security, wss:// provides additional benefits in modern web environments. Many browsers restrict ws:// connections from HTTPS pages due to mixed content policies, making wss:// necessary for applications served over HTTPS. Corporate firewalls and proxies are more likely to allow wss:// traffic on port 443 (the standard HTTPS port) compared to ws:// on port 80. Content delivery networks and load balancers also handle wss:// more reliably.
The performance overhead of wss:// encryption is minimal with modern hardware and optimized TLS implementations. The initial handshake adds a slight delay, but ongoing message encryption is extremely fast. Given the security benefits and near-zero performance cost, there's no legitimate reason to use ws:// in production environments. Only use ws:// for local development or in completely isolated networks where encryption is unnecessary.
Code Example:
// CLIENT-SIDE: Choosing between ws:// and wss://
// ❌ BAD: Unencrypted WebSocket (only for local development)
const unsecureWs = new WebSocket('ws://localhost:8080');
// All messages are sent in plain text
// Anyone on the network can read your data
// Vulnerable to man-in-the-middle attacks
// âś… GOOD: Encrypted WebSocket (production)
const secureWs = new WebSocket('wss://api.example.com');
// All messages are encrypted with TLS/SSL
// Protected from eavesdropping and tampering
// Required when page is served over HTTPS
// Dynamic protocol selection based on page protocol
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
// SERVER-SIDE: Setting up WSS with Node.js
// Method 1: Using HTTPS server with ws library
const https = require('https');
const fs = require('fs');
const WebSocket = require('ws');
// Load SSL/TLS certificates
const server = https.createServer({
cert: fs.readFileSync('/path/to/certificate.crt'),
key: fs.readFileSync('/path/to/private.key'),
// Optional: Certificate authority chain
ca: fs.readFileSync('/path/to/ca_bundle.crt')
});
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
console.log('Secure WebSocket connection established');
ws.on('message', (message) => {
// All messages automatically encrypted/decrypted by TLS layer
console.log('Received encrypted message:', message);
});
});
server.listen(443, () => {
console.log('WSS server running on port 443');
});
// Method 2: Using Let's Encrypt for free SSL certificates
const https = require('https');
const WebSocket = require('ws');
const greenlock = require('greenlock-express');
greenlock.init({
packageRoot: __dirname,
configDir: './greenlock.d',
maintainerEmail: 'admin@example.com',
cluster: false
}).ready((glx) => {
const server = glx.httpsServer();
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
console.log('Secure connection with auto-renewed certificate');
});
});
// Method 3: Behind a reverse proxy (recommended for production)
// Let nginx/Apache handle SSL termination
const http = require('http');
const WebSocket = require('ws');
const server = http.createServer();
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws, request) => {
// Check if request came through secure proxy
const isSecure = request.headers['x-forwarded-proto'] === 'https';
if (!isSecure && process.env.NODE_ENV === 'production') {
ws.close(1008, 'Secure connection required');
return;
}
console.log('Connection received via proxy');
});
server.listen(8080, '127.0.0.1', () => {
console.log('WebSocket server behind nginx proxy on port 8080');
});
// CLIENT-SIDE: Handling connection errors with fallback
class SecureWebSocketClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.ws = null;
}
connect() {
// Always try secure connection first
const wssUrl = `wss://${this.baseUrl}`;
console.log('Attempting secure connection:', wssUrl);
this.ws = new WebSocket(wssUrl);
this.ws.onopen = () => {
console.log('âś… Secure connection established');
};
this.ws.onerror = (error) => {
console.error('❌ WSS connection failed:', error);
// In development only, try fallback to ws://
if (window.location.hostname === 'localhost') {
console.warn('Falling back to unsecure connection (dev only)');
this.connectUnsecure();
} else {
console.error('Secure connection required in production');
}
};
}
connectUnsecure() {
const wsUrl = `ws://${this.baseUrl}`;
console.warn('⚠️ Using UNSECURE connection:', wsUrl);
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.warn('Unsecure connection established - for development only!');
};
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
// Data automatically encrypted when using wss://
this.ws.send(JSON.stringify(data));
}
}
}
// Usage
const client = new SecureWebSocketClient('api.example.com');
client.connect();
// NGINX configuration for WSS reverse proxy
/*
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL certificate configuration
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# WebSocket proxy configuration
location /ws {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# Recommended timeouts for WebSocket
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
}
*/
// Verify connection security in browser
function checkConnectionSecurity() {
const ws = new WebSocket('wss://api.example.com');
ws.onopen = () => {
console.log('Protocol:', ws.protocol);
console.log('URL:', ws.url);
// Check if connection is secure
if (ws.url.startsWith('wss://')) {
console.log('âś… Connection is encrypted with TLS/SSL');
} else {
console.warn('⚠️ Connection is NOT encrypted');
}
};
}
Mermaid Diagram (if helpful):
flowchart TD
subgraph "ws:// - Unencrypted"
A1[Client] -->|Plain Text| B1[Network]
B1 -->|Plain Text| C1[Server]
D1[Attacker] -.Can Read/Modify.-> B1
style D1 fill:#f88,stroke:#f00
end
subgraph "wss:// - Encrypted with TLS/SSL"
A2[Client] -->|TLS Handshake| B2[Encrypted Tunnel]
B2 -->|Encrypted Data| C2[Server]
D2[Attacker] -.Cannot Read.-> B2
style B2 fill:#8f8,stroke:#0f0
style D2 fill:#ccc,stroke:#666
end
subgraph "TLS Handshake Process"
E1[Client Hello] --> E2[Server Hello + Certificate]
E2 --> E3[Certificate Verification]
E3 --> E4[Key Exchange]
E4 --> E5[Encrypted Channel Established]
E5 --> E6[WebSocket Upgrade]
end
References:
- RFC 6455 - WebSocket Protocol (Section 11.1.2 on TLS)
- Mozilla WebSocket Security
- OWASP Transport Layer Protection
What are the security considerations for WebSocket connections?
The 30-Second Answer: WebSocket connections face unique security challenges including cross-site WebSocket hijacking (CSWSH), lack of built-in authentication, and vulnerability to man-in-the-middle attacks. Key considerations include using WSS (encrypted), validating origins, implementing proper authentication, rate limiting, input validation, and monitoring for suspicious connection patterns.
The 2-Minute Answer (If They Want More): WebSocket connections maintain persistent, bidirectional channels that bypass traditional HTTP security measures, creating specific vulnerabilities. Unlike HTTP requests, WebSockets don't automatically include CSRF tokens, and once established, they can send data freely without per-message authentication checks.
The most critical security considerations include: enforcing WSS (TLS/SSL encryption) to prevent eavesdropping and tampering; validating the Origin header to prevent unauthorized domains from establishing connections; implementing robust authentication mechanisms before upgrading the connection; applying rate limiting to prevent denial-of-service attacks; validating and sanitizing all incoming messages to prevent injection attacks; and implementing proper session management with timeouts.
Additional considerations include monitoring connection patterns for anomalies, implementing message size limits to prevent memory exhaustion, using subprotocol negotiation securely, protecting against reconnection storms, and ensuring proper error handling that doesn't leak sensitive information. Organizations should also implement logging and auditing of WebSocket connections, apply the principle of least privilege to connection permissions, and regularly update WebSocket libraries to patch known vulnerabilities.
Since WebSockets maintain long-lived connections, they're particularly vulnerable to resource exhaustion attacks, making connection limits and proper cleanup essential. The lack of automatic security features in the WebSocket protocol means developers must manually implement most security controls.
Code Example:
// Comprehensive WebSocket security implementation
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');
const wss = new WebSocket.Server({
noServer: true,
clientTracking: true,
maxPayload: 100 * 1024 // 100KB max message size
});
// Connection limit per IP
const connectionLimits = new Map();
const MAX_CONNECTIONS_PER_IP = 10;
// Rate limiter for messages
const messageLimiter = new Map();
server.on('upgrade', (request, socket, head) => {
// 1. Validate Origin
const origin = request.headers.origin;
const allowedOrigins = ['https://myapp.com', 'https://app.myapp.com'];
if (!allowedOrigins.includes(origin)) {
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
socket.destroy();
return;
}
// 2. Check IP-based connection limit
const clientIP = request.socket.remoteAddress;
const currentConnections = connectionLimits.get(clientIP) || 0;
if (currentConnections >= MAX_CONNECTIONS_PER_IP) {
socket.write('HTTP/1.1 429 Too Many Requests\r\n\r\n');
socket.destroy();
return;
}
// 3. Authenticate via token
const token = new URL(request.url, 'ws://localhost').searchParams.get('token');
try {
const user = jwt.verify(token, process.env.JWT_SECRET);
wss.handleUpgrade(request, socket, head, (ws) => {
// Track connection
connectionLimits.set(clientIP, currentConnections + 1);
ws.userId = user.id;
ws.userRole = user.role;
ws.connectedAt = Date.now();
wss.emit('connection', ws, request);
});
} catch (err) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
}
});
wss.on('connection', (ws, request) => {
const clientIP = request.socket.remoteAddress;
// Initialize rate limiter for this connection
messageLimiter.set(ws, { count: 0, resetAt: Date.now() + 60000 });
// 4. Set connection timeout
const timeout = setTimeout(() => {
ws.close(1000, 'Connection timeout');
}, 30 * 60 * 1000); // 30 minutes
ws.on('message', (data) => {
// 5. Rate limiting per connection
const limiter = messageLimiter.get(ws);
const now = Date.now();
if (now > limiter.resetAt) {
limiter.count = 0;
limiter.resetAt = now + 60000;
}
limiter.count++;
if (limiter.count > 100) { // Max 100 messages per minute
ws.close(1008, 'Rate limit exceeded');
return;
}
// 6. Validate message size (already enforced by maxPayload, but double-check)
if (data.length > 100 * 1024) {
ws.close(1009, 'Message too large');
return;
}
try {
// 7. Validate and sanitize input
const message = JSON.parse(data);
if (!message.type || typeof message.type !== 'string') {
ws.send(JSON.stringify({ error: 'Invalid message format' }));
return;
}
// 8. Implement authorization checks
if (message.type === 'admin_action' && ws.userRole !== 'admin') {
ws.send(JSON.stringify({ error: 'Unauthorized action' }));
return;
}
// Reset timeout on activity
clearTimeout(timeout);
// Process valid message
handleMessage(ws, message);
} catch (err) {
// 9. Safe error handling - don't leak internal details
console.error('Message processing error:', err);
ws.send(JSON.stringify({ error: 'Invalid message' }));
}
});
ws.on('close', () => {
// 10. Cleanup resources
clearTimeout(timeout);
messageLimiter.delete(ws);
const currentConnections = connectionLimits.get(clientIP) || 0;
if (currentConnections > 0) {
connectionLimits.set(clientIP, currentConnections - 1);
}
});
ws.on('error', (err) => {
console.error('WebSocket error:', err);
// Don't send error details to client
});
});
// Periodic cleanup of stale connection limits
setInterval(() => {
connectionLimits.forEach((count, ip) => {
if (count === 0) {
connectionLimits.delete(ip);
}
});
}, 5 * 60 * 1000); // Every 5 minutes
Mermaid Diagram (if helpful):
flowchart TD
A[Client Connection Request] --> B{Origin Valid?}
B -->|No| C[Reject 403]
B -->|Yes| D{IP Connection Limit OK?}
D -->|No| E[Reject 429]
D -->|Yes| F{Token Valid?}
F -->|No| G[Reject 401]
F -->|Yes| H[Upgrade to WebSocket]
H --> I[Connection Established]
I --> J[Message Received]
J --> K{Rate Limit OK?}
K -->|No| L[Close Connection]
K -->|Yes| M{Message Size OK?}
M -->|No| L
M -->|Yes| N{Valid Format?}
N -->|No| O[Send Error]
N -->|Yes| P{Authorized?}
P -->|No| O
P -->|Yes| Q[Process Message]
Q --> R{Connection Active?}
R -->|Yes| J
R -->|No| S[Cleanup Resources]
References:
- OWASP WebSocket Security
- RFC 6455 - WebSocket Protocol Security Considerations
- Mozilla WebSocket Security
Libraries and Implementation
What is the difference between native WebSocket and Socket.IO?
The 30-Second Answer: Native WebSocket is a low-level browser API providing bidirectional communication over a persistent TCP connection, while Socket.IO is a high-level library that uses WebSocket as its primary transport but adds features like automatic reconnection, rooms, fallback protocols, and a custom event system with acknowledgments.
The 2-Minute Answer (If They Want More):
Native WebSocket is a standardized protocol (RFC 6455) built into modern browsers and provides a simple API for sending and receiving messages. You open a connection, send messages with send(), and receive them via the onmessage event. It's lightweight and works with any WebSocket server, but you're responsible for implementing reconnection logic, heartbeats, message formatting, and error handling yourself.
Socket.IO, on the other hand, is a complete framework that abstracts away many complexity concerns. It uses WebSocket when available but can fall back to HTTP long-polling in restrictive environments. This makes it more reliable in corporate networks or situations where WebSocket connections are blocked. Socket.IO also implements a custom protocol on top of WebSocket, which means a Socket.IO client cannot connect to a standard WebSocket server and vice versa.
The feature differences are significant. Socket.IO provides automatic reconnection with configurable strategies, room-based broadcasting for organizing connections, namespaces for multiplexing, acknowledgment callbacks to confirm message delivery, and binary event support. It also handles connection state management, including detecting disconnections through heartbeat mechanisms. Native WebSocket provides none of these features out of the box.
The tradeoff is complexity and bundle size. Native WebSocket has zero dependencies and minimal overhead, while Socket.IO adds approximately 60KB to your client bundle. For simple use cases or when you need to connect to a standard WebSocket server, native WebSocket is the better choice. For complex applications requiring reliable connections, rooms, and automatic reconnection, Socket.IO's features justify the additional overhead.
Code Example:
// ===== NATIVE WEBSOCKET =====
// Client-side
const ws = new WebSocket('ws://localhost:8080');
// Simple event handlers
ws.onopen = () => {
console.log('Connected');
ws.send(JSON.stringify({ type: 'hello', data: 'world' }));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Received:', message);
};
ws.onerror = (error) => {
console.error('Error:', error);
};
ws.onclose = () => {
console.log('Disconnected - manual reconnection required');
// You must implement reconnection yourself
};
// Server-side (using 'ws' library)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const message = JSON.parse(data);
// Broadcast to all clients manually
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
});
});
// ===== SOCKET.IO =====
// Client-side
const socket = io('http://localhost:3000', {
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000
});
// Named events with acknowledgments
socket.emit('hello', { data: 'world' }, (response) => {
console.log('Acknowledged:', response);
});
socket.on('custom-event', (data) => {
console.log('Received:', data);
});
// Automatic reconnection events
socket.on('reconnect', (attemptNumber) => {
console.log('Reconnected after', attemptNumber, 'attempts');
});
// Server-side
const { Server } = require('socket.io');
const io = new Server(3000);
io.on('connection', (socket) => {
socket.on('hello', (data, callback) => {
// Join rooms
socket.join('room1');
// Broadcast to room
io.to('room1').emit('custom-event', data);
// Send acknowledgment
callback({ status: 'received' });
});
});
Comparison Table:
| Feature | Native WebSocket | Socket.IO |
|---|---|---|
| Protocol | Standard RFC 6455 | Custom protocol |
| Transport | WebSocket only | WebSocket + fallbacks |
| Reconnection | Manual | Automatic |
| Bundle Size | ~0KB (built-in) | ~60KB |
| Room Support | No | Yes |
| Namespaces | No | Yes |
| Acknowledgments | No | Yes |
| Binary Support | Yes | Yes (enhanced) |
| Compatibility | Any WebSocket server | Socket.IO servers only |
| Heartbeat | Manual | Automatic |
References:
↑ Back to topWhat is Socket.IO and how does it enhance WebSocket?
The 30-Second Answer: Socket.IO is a JavaScript library that provides a higher-level abstraction over WebSocket with automatic fallback to HTTP long-polling when WebSocket isn't available. It adds features like automatic reconnection, rooms, namespaces, broadcasting, and acknowledgments that aren't part of the native WebSocket API.
The 2-Minute Answer (If They Want More): Socket.IO is built on top of the Engine.IO library and provides a robust real-time communication layer that goes beyond what native WebSocket offers. While WebSocket is a transport protocol, Socket.IO is a complete framework for real-time applications. It handles connection establishment with automatic transport selection, starting with WebSocket but falling back to HTTP long-polling in restrictive network environments.
The library adds essential features that you'd otherwise have to build yourself: automatic reconnection with exponential backoff, heartbeat mechanisms to detect disconnections, built-in event emitters for cleaner message handling, and rooms/namespaces for organizing connections. Socket.IO also handles binary data seamlessly and provides acknowledgment callbacks to ensure messages are received.
One of Socket.IO's biggest advantages is cross-browser compatibility and handling of edge cases. It works around corporate firewalls, proxies, and personal firewalls that might block WebSocket connections. The library also provides a consistent API across different environments, making it easier to build reliable real-time applications without worrying about low-level connection management.
However, this convenience comes with tradeoffs. Socket.IO adds overhead in terms of bundle size (around 60KB minified) and uses a custom protocol on top of WebSocket, meaning Socket.IO clients can only connect to Socket.IO servers. For simple use cases where you control both client and server and don't need the extra features, native WebSocket might be more appropriate.
Code Example:
// Server-side (Node.js with Socket.IO)
const { Server } = require('socket.io');
const io = new Server(3000, {
cors: { origin: '*' }
});
// Built-in room management
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
// Join a room
socket.join('notifications');
// Handle custom events with acknowledgments
socket.on('send-message', (data, callback) => {
// Broadcast to all clients in a room except sender
socket.to('notifications').emit('new-message', data);
// Acknowledge receipt
callback({ status: 'delivered', timestamp: Date.now() });
});
// Automatic disconnect handling
socket.on('disconnect', (reason) => {
console.log('Client disconnected:', reason);
});
});
// Client-side
const socket = io('http://localhost:3000', {
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5
});
// Automatic reconnection events
socket.on('connect', () => {
console.log('Connected with ID:', socket.id);
});
socket.on('reconnect_attempt', (attemptNumber) => {
console.log('Reconnection attempt:', attemptNumber);
});
// Send message with acknowledgment
socket.emit('send-message', { text: 'Hello!' }, (response) => {
console.log('Server acknowledged:', response.status);
});
// Listen for broadcasts
socket.on('new-message', (data) => {
console.log('Received:', data);
});
Mermaid Diagram (if helpful):
flowchart TD
A[Socket.IO Client] -->|Connection Request| B{Transport Negotiation}
B -->|WebSocket Available| C[WebSocket Transport]
B -->|WebSocket Blocked| D[HTTP Long-Polling]
C --> E[Socket.IO Protocol Layer]
D --> E
E --> F[Event System]
E --> G[Room Management]
E --> H[Auto Reconnection]
E --> I[Acknowledgments]
F --> J[Application Logic]
G --> J
H --> J
I --> J
References:
↑ Back to topWhat are the best practices for WebSocket error handling?
The 30-Second Answer: I implement comprehensive error handling by managing connection lifecycle events (error, close), implementing automatic reconnection with exponential backoff, validating all incoming messages, using try-catch blocks for message parsing, implementing timeout mechanisms, and providing meaningful error feedback to both clients and server-side logging systems.
The 2-Minute Answer (If They Want More): Effective WebSocket error handling requires addressing multiple failure scenarios: network interruptions, server errors, invalid messages, authentication failures, and resource exhaustion. I start by implementing robust connection state management, tracking whether connections are opening, open, closing, or closed, and handling transitions between these states appropriately.
On the client side, I implement automatic reconnection with exponential backoff to handle temporary network issues without overwhelming the server. I track reconnection attempts, set maximum retry limits, and provide user feedback about connection status. I also implement circuit breaker patterns for repeated failures, temporarily stopping reconnection attempts when the server appears to be down for extended periods.
Message-level error handling involves validating all incoming data before processing, using try-catch blocks around JSON parsing and message handlers, implementing message schema validation for complex applications, and handling malformed or unexpected message types gracefully. I also implement message acknowledgment patterns to ensure critical messages are received and processed successfully.
Server-side error handling focuses on preventing crashes and resource leaks. I implement error handlers on all event listeners, properly clean up resources when connections close or error, implement rate limiting to prevent abuse, set connection and message size limits, and use heartbeat mechanisms to detect and close dead connections. I also implement comprehensive logging and monitoring to track error patterns and identify systemic issues before they impact users.
Code Example:
// ===== CLIENT-SIDE ERROR HANDLING =====
class RobustWebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
this.reconnectDelay = options.reconnectDelay || 1000;
this.maxReconnectDelay = options.maxReconnectDelay || 30000;
this.listeners = new Map();
this.messageQueue = [];
this.connectionState = 'disconnected';
this.heartbeatInterval = null;
this.connect();
}
connect() {
try {
this.connectionState = 'connecting';
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.connectionState = 'connected';
this.reconnectAttempts = 0;
this.emit('connected');
// Send queued messages
this.flushMessageQueue();
// Start heartbeat
this.startHeartbeat();
};
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
// Handle heartbeat responses
if (message.type === 'pong') {
return;
}
// Validate message structure
if (!message.type || !message.data) {
console.error('Invalid message structure:', message);
return;
}
// Emit to registered listeners
this.emit(message.type, message.data);
} catch (error) {
console.error('Message parsing error:', error);
this.emit('error', {
type: 'parse_error',
message: 'Failed to parse server message',
originalData: event.data
});
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.connectionState = 'error';
this.emit('error', {
type: 'connection_error',
message: 'Connection error occurred',
error
});
};
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.connectionState = 'disconnected';
this.stopHeartbeat();
// Emit close event
this.emit('disconnected', {
code: event.code,
reason: event.reason,
wasClean: event.wasClean
});
// Attempt reconnection for non-clean closes
if (!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) {
this.scheduleReconnect();
} else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.emit('max_reconnect_attempts', {
message: 'Maximum reconnection attempts reached'
});
}
};
} catch (error) {
console.error('Connection initialization error:', error);
this.scheduleReconnect();
}
}
scheduleReconnect() {
this.reconnectAttempts++;
// Exponential backoff with jitter
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
this.maxReconnectDelay
) + Math.random() * 1000;
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
this.emit('reconnecting', {
attempt: this.reconnectAttempts,
delay
});
setTimeout(() => this.connect(), delay);
}
send(type, data) {
const message = JSON.stringify({ type, data, timestamp: Date.now() });
if (this.connectionState === 'connected' && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(message);
} catch (error) {
console.error('Send error:', error);
this.messageQueue.push({ type, data });
this.emit('error', {
type: 'send_error',
message: 'Failed to send message',
error
});
}
} else {
// Queue message for later
this.messageQueue.push({ type, data });
console.warn('Message queued - connection not ready');
}
}
flushMessageQueue() {
while (this.messageQueue.length > 0) {
const { type, data } = this.messageQueue.shift();
this.send(type, data);
}
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.connectionState === 'connected') {
this.send('ping', { timestamp: Date.now() });
}
}, 30000);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
emit(event, data) {
const callbacks = this.listeners.get(event) || [];
callbacks.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`Listener error for event '${event}':`, error);
}
});
}
close() {
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
this.stopHeartbeat();
if (this.ws) {
this.ws.close(1000, 'Client closed connection');
}
}
}
// Usage
const client = new RobustWebSocketClient('ws://localhost:8080', {
maxReconnectAttempts: 10,
reconnectDelay: 1000,
maxReconnectDelay: 30000
});
client.on('connected', () => {
console.log('Successfully connected!');
});
client.on('error', (error) => {
console.error('Error occurred:', error);
// Show user notification
});
client.on('reconnecting', ({ attempt, delay }) => {
console.log(`Reconnecting (${attempt})...`);
// Update UI status
});
client.on('max_reconnect_attempts', () => {
console.error('Cannot connect to server');
// Show user error message
});
// ===== SERVER-SIDE ERROR HANDLING =====
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 8080,
// Set limits to prevent resource exhaustion
maxPayload: 100 * 1024, // 100KB max message size
perMessageDeflate: false // Disable compression if not needed
});
// Track connection metrics
const connectionMetrics = {
total: 0,
active: 0,
errors: 0,
messageCount: 0
};
wss.on('connection', (ws, req) => {
connectionMetrics.total++;
connectionMetrics.active++;
// Set timeout for idle connections
const connectionTimeout = setTimeout(() => {
console.log('Closing idle connection');
ws.close(1000, 'Connection timeout');
}, 5 * 60 * 1000); // 5 minutes
// Rate limiting
let messageCount = 0;
let rateLimitReset = Date.now();
const MAX_MESSAGES_PER_MINUTE = 60;
ws.on('message', (data) => {
try {
// Reset rate limit counter every minute
if (Date.now() - rateLimitReset > 60000) {
messageCount = 0;
rateLimitReset = Date.now();
}
// Check rate limit
if (++messageCount > MAX_MESSAGES_PER_MINUTE) {
ws.send(JSON.stringify({
type: 'error',
data: { message: 'Rate limit exceeded' }
}));
return;
}
// Reset idle timeout on activity
clearTimeout(connectionTimeout);
const message = JSON.parse(data);
connectionMetrics.messageCount++;
// Validate message
if (!message.type || message.data === undefined) {
throw new Error('Invalid message structure');
}
// Process message
handleMessage(ws, message);
} catch (error) {
console.error('Message handling error:', error);
connectionMetrics.errors++;
// Send error response to client
try {
ws.send(JSON.stringify({
type: 'error',
data: {
message: 'Message processing failed',
details: error.message
}
}));
} catch (sendError) {
console.error('Failed to send error response:', sendError);
}
}
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
connectionMetrics.errors++;
// Don't try to send error to client - connection may be broken
});
ws.on('close', () => {
connectionMetrics.active--;
clearTimeout(connectionTimeout);
console.log(`Connection closed. Active: ${connectionMetrics.active}`);
});
});
// Global error handler
wss.on('error', (error) => {
console.error('WebSocket server error:', error);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, closing server...');
wss.close(() => {
console.log('Server closed');
process.exit(0);
});
});
function handleMessage(ws, message) {
// Message handling logic with error boundaries
switch (message.type) {
case 'ping':
ws.send(JSON.stringify({ type: 'pong', data: message.data }));
break;
default:
// Handle other message types
break;
}
}
References:
↑ Back to topReliability and Reconnection
What is heartbeat/ping-pong in WebSocket and why is it important?
The 30-Second Answer: Heartbeat (ping-pong) is a mechanism where the client and server periodically exchange small messages to detect if the connection is still alive. The server sends a ping frame, and the client automatically responds with a pong frame. This prevents silent connection failures and helps detect dead connections before they cause issues.
The 2-Minute Answer (If They Want More): WebSocket connections can fail silently due to network issues, proxy timeouts, or firewall rules. Without heartbeat, your application might think it's connected when the underlying TCP connection is actually dead. This leads to lost messages and poor user experience.
The WebSocket protocol includes built-in ping/pong frames (opcode 0x9 and 0xA). The server typically sends ping frames at regular intervals (every 30-60 seconds), and browsers automatically respond with pong frames - you don't need to handle this in JavaScript. However, the browser's WebSocket API doesn't expose these frames to your code, so you can't rely on them for application-level keepalive.
For robust applications, I implement application-level heartbeat on top of the protocol-level ping/pong. I send a JSON message like {"type": "ping"} every 30 seconds and expect a {"type": "pong"} response within 5 seconds. If I don't receive it, I assume the connection is dead and trigger reconnection. This also works through proxies that might not forward protocol-level ping/pong frames.
Heartbeat also prevents idle timeout disconnections. Many load balancers, proxies, and firewalls close connections after 60 seconds of inactivity. Regular heartbeat messages keep the connection alive. The timing is critical: too frequent wastes bandwidth and battery, too infrequent allows connections to die before detection.
Code Example:
class HeartbeatWebSocket {
constructor(url, options = {}) {
this.url = url;
this.ws = null;
this.heartbeatInterval = options.heartbeatInterval || 30000; // 30 seconds
this.heartbeatTimeout = options.heartbeatTimeout || 5000; // 5 seconds
this.heartbeatTimer = null;
this.heartbeatTimeoutTimer = null;
this.missedHeartbeats = 0;
this.maxMissedHeartbeats = options.maxMissedHeartbeats || 3;
this.onopen = null;
this.onmessage = null;
this.onerror = null;
this.onclose = null;
this.onheartbeatfailed = null;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = (event) => {
console.log('WebSocket connected, starting heartbeat');
this.missedHeartbeats = 0;
this.startHeartbeat();
if (this.onopen) this.onopen(event);
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Handle pong response
if (data.type === 'pong') {
console.log('Received pong, connection alive');
this.missedHeartbeats = 0;
// Clear timeout timer
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
return;
}
// Handle server-initiated ping (respond with pong)
if (data.type === 'ping') {
console.log('Received ping from server, sending pong');
this.ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
return;
}
// Pass other messages to handler
if (this.onmessage) this.onmessage(event);
};
this.ws.onerror = (event) => {
console.error('WebSocket error:', event);
if (this.onerror) this.onerror(event);
};
this.ws.onclose = (event) => {
console.log('WebSocket closed');
this.stopHeartbeat();
if (this.onclose) this.onclose(event);
};
}
startHeartbeat() {
// Clear any existing heartbeat
this.stopHeartbeat();
// Send initial ping
this.sendPing();
// Schedule periodic pings
this.heartbeatTimer = setInterval(() => {
this.sendPing();
}, this.heartbeatInterval);
}
sendPing() {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.warn('Cannot send ping, connection not open');
return;
}
console.log('Sending ping');
this.ws.send(JSON.stringify({
type: 'ping',
timestamp: Date.now()
}));
// Set timeout for pong response
this.heartbeatTimeoutTimer = setTimeout(() => {
this.missedHeartbeats++;
console.warn(`Missed heartbeat ${this.missedHeartbeats}/${this.maxMissedHeartbeats}`);
if (this.missedHeartbeats >= this.maxMissedHeartbeats) {
console.error('Too many missed heartbeats, connection assumed dead');
this.handleHeartbeatFailure();
}
}, this.heartbeatTimeout);
}
handleHeartbeatFailure() {
this.stopHeartbeat();
if (this.onheartbeatfailed) {
this.onheartbeatfailed(this.missedHeartbeats);
}
// Close the dead connection and trigger reconnection
if (this.ws) {
this.ws.close(1000, 'Heartbeat failure');
}
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
console.error('Cannot send message, connection not open');
}
}
close() {
this.stopHeartbeat();
if (this.ws) {
this.ws.close(1000, 'Client closing connection');
}
}
}
// Usage example with automatic reconnection
class RobustWebSocket {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
}
connect() {
this.ws = new HeartbeatWebSocket(this.url, {
heartbeatInterval: 30000,
heartbeatTimeout: 5000,
maxMissedHeartbeats: 3
});
this.ws.onopen = () => {
console.log('Connection established');
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received message:', data);
};
this.ws.onheartbeatfailed = (missedCount) => {
console.error(`Heartbeat failed after ${missedCount} missed beats`);
this.scheduleReconnect();
};
this.ws.onclose = (event) => {
console.log('Connection closed:', event.code);
if (event.code !== 1000) { // Not normal closure
this.scheduleReconnect();
}
};
this.ws.connect();
}
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
return;
}
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => this.connect(), delay);
}
send(data) {
if (this.ws) {
this.ws.send(data);
}
}
close() {
if (this.ws) {
this.ws.close();
}
}
}
// Initialize
const robustWs = new RobustWebSocket('wss://api.example.com/ws');
robustWs.connect();
Mermaid Diagram (if helpful):
sequenceDiagram
participant Client
participant Server
Note over Client,Server: Connection Established
loop Every 30 seconds
Client->>Server: {"type": "ping", "timestamp": ...}
alt Connection Alive
Server-->>Client: {"type": "pong", "timestamp": ...}
Note over Client: Reset missed counter
else Connection Dead (5s timeout)
Note over Client: Increment missed counter
alt Missed < 3
Note over Client: Wait for next interval
else Missed >= 3
Client->>Client: Close connection
Client->>Client: Trigger reconnection
end
end
end
References:
↑ Back to topHow do you handle network interruptions gracefully?
The 30-Second Answer: I handle network interruptions by implementing automatic reconnection with exponential backoff, queueing messages during downtime, showing clear connection status to users, and gracefully degrading functionality. I use the browser's online/offline events to detect network changes proactively and maintain application state across reconnections.
The 2-Minute Answer (If They Want More): Graceful handling of network interruptions is essential for good user experience. Users expect applications to recover automatically without losing data or requiring manual intervention.
First, I implement comprehensive connection state management. The application tracks whether it's online, offline, connecting, or reconnecting, and updates the UI accordingly. I use the browser's navigator.onLine property and online/offline events to detect network availability changes immediately, rather than waiting for WebSocket timeout.
Second, I queue all outgoing messages during disconnection and replay them after reconnection. This ensures no data loss. For incoming messages, I implement sequence numbers or timestamps so the server can resend any messages the client missed during downtime. Some applications use event sourcing or CQRS patterns to maintain a consistent view of server state.
Third, I implement progressive enhancement: when disconnected, I disable real-time features but keep the application usable with cached data. For example, in a chat app, users can still read message history and compose new messages - they're just queued for delivery.
Fourth, I provide clear feedback. Users should never wonder if their action succeeded. I show connection status indicators, display "sending..." states for queued messages, and notify users when the connection is restored. I avoid blocking the entire UI - users should be able to navigate and interact with local data even when offline.
Code Example:
class GracefulWebSocket {
constructor(url, options = {}) {
this.url = url;
this.ws = null;
this.state = 'disconnected'; // online, offline, connecting, reconnecting, disconnected
this.messageQueue = [];
this.messageIdCounter = 0;
this.pendingMessages = new Map(); // Track messages awaiting acknowledgment
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
this.baseDelay = options.baseDelay || 1000;
this.maxDelay = options.maxDelay || 30000;
// Event handlers
this.onStateChange = options.onStateChange || null;
this.onMessageQueued = options.onMessageQueued || null;
this.onMessageSent = options.onMessageSent || null;
this.onMessageReceived = options.onMessageReceived || null;
// Listen for browser online/offline events
window.addEventListener('online', () => this.handleBrowserOnline());
window.addEventListener('offline', () => this.handleBrowserOffline());
// Check initial state
if (!navigator.onLine) {
this.setState('offline');
}
}
setState(newState) {
const oldState = this.state;
this.state = newState;
console.log(`Connection state: ${oldState} -> ${newState}`);
if (this.onStateChange) {
this.onStateChange(newState, oldState);
}
}
handleBrowserOnline() {
console.log('Browser detected network connection');
if (this.state === 'offline') {
this.connect();
}
}
handleBrowserOffline() {
console.log('Browser detected network loss');
this.setState('offline');
// Close existing connection
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
connect() {
// Don't attempt connection if browser is offline
if (!navigator.onLine) {
console.log('Browser is offline, deferring connection');
this.setState('offline');
return;
}
if (this.state === 'connecting' || this.state === 'online') {
return; // Already connecting or connected
}
const isReconnect = this.reconnectAttempts > 0;
this.setState(isReconnect ? 'reconnecting' : 'connecting');
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.setState('online');
this.reconnectAttempts = 0;
// Flush queued messages
this.flushMessageQueue();
// Request missed messages from server
this.requestMissedMessages();
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleMessage(data);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.ws = null;
// Only reconnect if we're not intentionally offline
if (this.state !== 'offline' && navigator.onLine) {
this.scheduleReconnect();
} else {
this.setState('disconnected');
}
};
} catch (error) {
console.error('Failed to create WebSocket:', error);
this.setState('disconnected');
this.scheduleReconnect();
}
}
handleMessage(data) {
// Handle acknowledgments
if (data.type === 'ack' && data.messageId) {
this.handleAcknowledgment(data.messageId);
return;
}
// Handle regular messages
if (this.onMessageReceived) {
this.onMessageReceived(data);
}
}
handleAcknowledgment(messageId) {
if (this.pendingMessages.has(messageId)) {
const message = this.pendingMessages.get(messageId);
this.pendingMessages.delete(messageId);
console.log(`Message ${messageId} acknowledged`);
if (this.onMessageSent) {
this.onMessageSent(message);
}
}
}
send(data, options = {}) {
const messageId = ++this.messageIdCounter;
const message = {
id: messageId,
data: data,
timestamp: Date.now(),
requiresAck: options.requiresAck !== false // Default true
};
if (this.state === 'online' && this.ws.readyState === WebSocket.OPEN) {
this.sendMessage(message);
} else {
// Queue message for later delivery
console.log(`Connection not ready (state: ${this.state}), queuing message ${messageId}`);
this.messageQueue.push(message);
if (this.onMessageQueued) {
this.onMessageQueued(message);
}
// Attempt to reconnect if we're disconnected
if (this.state === 'disconnected' && navigator.onLine) {
this.connect();
}
}
return messageId;
}
sendMessage(message) {
const payload = {
id: message.id,
...message.data,
timestamp: message.timestamp
};
this.ws.send(JSON.stringify(payload));
// Track message if acknowledgment is required
if (message.requiresAck) {
this.pendingMessages.set(message.id, message);
// Set timeout for acknowledgment
setTimeout(() => {
if (this.pendingMessages.has(message.id)) {
console.warn(`Message ${message.id} not acknowledged, requeueing`);
this.pendingMessages.delete(message.id);
this.messageQueue.unshift(message); // Add to front of queue
}
}, 10000); // 10 second timeout
}
}
flushMessageQueue() {
console.log(`Flushing ${this.messageQueue.length} queued messages`);
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.sendMessage(message);
}
}
requestMissedMessages() {
// Request any messages we missed during disconnection
const lastMessageTimestamp = this.getLastMessageTimestamp();
if (lastMessageTimestamp) {
this.ws.send(JSON.stringify({
type: 'sync',
since: lastMessageTimestamp
}));
}
}
getLastMessageTimestamp() {
// Retrieve from localStorage or application state
return localStorage.getItem('lastMessageTimestamp') || null;
}
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
this.setState('disconnected');
return;
}
this.reconnectAttempts++;
// Calculate delay with exponential backoff and jitter
const exponentialDelay = Math.min(
this.baseDelay * Math.pow(2, this.reconnectAttempts - 1),
this.maxDelay
);
const jitter = exponentialDelay * (0.1 + Math.random() * 0.2);
const delay = exponentialDelay + jitter;
console.log(`Scheduling reconnection in ${Math.round(delay / 1000)}s (attempt ${this.reconnectAttempts})`);
setTimeout(() => {
console.log(`Attempting reconnection ${this.reconnectAttempts}...`);
this.connect();
}, delay);
}
getState() {
return this.state;
}
getQueuedMessageCount() {
return this.messageQueue.length;
}
getPendingMessageCount() {
return this.pendingMessages.size;
}
close() {
this.setState('disconnected');
if (this.ws) {
this.ws.close(1000, 'Client closing connection');
this.ws = null;
}
}
}
// UI Integration Example
class ChatApplication {
constructor() {
this.ws = new GracefulWebSocket('wss://chat.example.com/ws', {
onStateChange: (newState, oldState) => this.handleStateChange(newState, oldState),
onMessageQueued: (message) => this.showMessageQueued(message),
onMessageSent: (message) => this.showMessageSent(message),
onMessageReceived: (data) => this.handleIncomingMessage(data)
});
this.ws.connect();
}
handleStateChange(newState, oldState) {
const statusIndicator = document.getElementById('connection-status');
const statusText = document.getElementById('connection-text');
switch (newState) {
case 'online':
statusIndicator.className = 'status-online';
statusText.textContent = 'Connected';
this.showNotification('Connection restored', 'success');
break;
case 'offline':
statusIndicator.className = 'status-offline';
statusText.textContent = 'No internet connection';
this.showNotification('You are offline. Messages will be sent when connection is restored.', 'warning');
break;
case 'connecting':
statusIndicator.className = 'status-connecting';
statusText.textContent = 'Connecting...';
break;
case 'reconnecting':
statusIndicator.className = 'status-reconnecting';
const queuedCount = this.ws.getQueuedMessageCount();
statusText.textContent = `Reconnecting... (${queuedCount} messages queued)`;
break;
case 'disconnected':
statusIndicator.className = 'status-disconnected';
statusText.textContent = 'Disconnected';
this.showNotification('Connection lost. Trying to reconnect...', 'error');
break;
}
}
showMessageQueued(message) {
// Show visual feedback that message is queued
const messageElement = document.getElementById(`message-${message.id}`);
if (messageElement) {
messageElement.classList.add('message-queued');
messageElement.querySelector('.status').textContent = 'Queued';
}
}
showMessageSent(message) {
// Update UI to show message was delivered
const messageElement = document.getElementById(`message-${message.id}`);
if (messageElement) {
messageElement.classList.remove('message-queued');
messageElement.classList.add('message-sent');
messageElement.querySelector('.status').textContent = 'Sent';
}
}
handleIncomingMessage(data) {
// Display incoming message
this.addMessageToUI(data);
// Store timestamp for sync
localStorage.setItem('lastMessageTimestamp', data.timestamp);
}
sendMessage(text) {
const messageId = this.ws.send({
type: 'chat',
text: text,
userId: this.userId
});
// Optimistically add to UI
this.addMessageToUI({
id: messageId,
text: text,
userId: this.userId,
status: this.ws.getState() === 'online' ? 'Sending...' : 'Queued'
});
}
showNotification(message, type) {
// Show toast/snackbar notification
console.log(`[${type.toUpperCase()}] ${message}`);
}
addMessageToUI(data) {
// Add message to chat UI
console.log('Adding message to UI:', data);
}
}
// Initialize application
const chat = new ChatApplication();
Mermaid Diagram (if helpful):
stateDiagram-v2
[*] --> Disconnected
Disconnected --> Offline: Browser offline event
Disconnected --> Connecting: connect()
Connecting --> Online: WebSocket opened
Connecting --> Reconnecting: Connection failed
Connecting --> Offline: Browser offline event
Online --> Reconnecting: Connection lost
Online --> Offline: Browser offline event
Online --> Disconnected: close()
Reconnecting --> Online: WebSocket opened
Reconnecting --> Offline: Browser offline event
Reconnecting --> Disconnected: Max attempts reached
Offline --> Connecting: Browser online event
note right of Connecting
Attempt initial connection
No queued messages yet
end note
note right of Reconnecting
Exponential backoff
Messages queued
Show queue count in UI
end note
note right of Online
Flush message queue
Request missed messages
Normal operation
end note
note right of Offline
Queue all messages
Disable real-time features
Show offline indicator
end note
References:
↑ Back to topScalability and Architecture
What is the pub/sub pattern in WebSocket architecture?
The 30-Second Answer: The pub/sub (publish/subscribe) pattern allows WebSocket servers to broadcast messages through channels without tight coupling between senders and receivers. Clients subscribe to specific channels and receive all messages published to those channels, enabling scalable real-time communication across multiple servers.
The 2-Minute Answer (If They Want More): The publish/subscribe pattern is a messaging paradigm where publishers send messages to named channels without knowing who will receive them, and subscribers express interest in channels without knowing who publishes to them. This decoupling is essential for scalable WebSocket architectures.
In a multi-server WebSocket setup, pub/sub solves the critical problem of cross-server communication. When a message arrives at Server A, but the recipient is connected to Server B, the pub/sub broker ensures message delivery. Server A publishes to a channel, and all servers (including Server B) receive it and forward to their relevant connected clients.
The pattern enables several powerful features: room-based communication where users join/leave channels dynamically, selective message delivery based on subscriptions, and horizontal scaling without application code changes. Popular implementations use Redis Pub/Sub, RabbitMQ, Apache Kafka, or cloud services like AWS SNS/SQS.
I typically implement channel namespacing (e.g., chat:room:123, notifications:user:456) to organize subscriptions, use wildcards for pattern matching, and handle subscription state carefully during reconnections. The broker becomes the single source of truth for message distribution, while individual servers only manage their local WebSocket connections.
Code Example:
// Advanced pub/sub implementation with Redis for WebSocket rooms
const express = require('express');
const { createServer } = require('http');
const { WebSocketServer } = require('ws');
const Redis = require('ioredis');
const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });
// Redis clients for pub/sub
const publisher = new Redis();
const subscriber = new Redis();
// Track client subscriptions and metadata
class ConnectionManager {
constructor() {
this.connections = new Map(); // clientId -> { ws, userId, subscriptions: Set }
this.rooms = new Map(); // roomId -> Set of clientIds
}
addConnection(clientId, ws, userId) {
this.connections.set(clientId, {
ws,
userId,
subscriptions: new Set()
});
}
removeConnection(clientId) {
const conn = this.connections.get(clientId);
if (conn) {
// Unsubscribe from all rooms
conn.subscriptions.forEach(roomId => {
this.leaveRoom(clientId, roomId);
});
this.connections.delete(clientId);
}
}
joinRoom(clientId, roomId) {
const conn = this.connections.get(clientId);
if (!conn) return false;
conn.subscriptions.add(roomId);
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, new Set());
// Subscribe to Redis channel when first client joins
subscriber.subscribe(`room:${roomId}`);
}
this.rooms.get(roomId).add(clientId);
return true;
}
leaveRoom(clientId, roomId) {
const conn = this.connections.get(clientId);
if (conn) {
conn.subscriptions.delete(roomId);
}
const room = this.rooms.get(roomId);
if (room) {
room.delete(clientId);
// Unsubscribe from Redis if no local clients in room
if (room.size === 0) {
this.rooms.delete(roomId);
subscriber.unsubscribe(`room:${roomId}`);
}
}
}
getClientsInRoom(roomId) {
return this.rooms.get(roomId) || new Set();
}
sendToClient(clientId, message) {
const conn = this.connections.get(clientId);
if (conn && conn.ws.readyState === conn.ws.OPEN) {
conn.ws.send(JSON.stringify(message));
return true;
}
return false;
}
broadcastToRoom(roomId, message, excludeClientId = null) {
const clients = this.getClientsInRoom(roomId);
let sent = 0;
clients.forEach(clientId => {
if (clientId !== excludeClientId) {
if (this.sendToClient(clientId, message)) {
sent++;
}
}
});
return sent;
}
}
const manager = new ConnectionManager();
// Handle incoming messages from Redis (from other servers)
subscriber.on('message', (channel, message) => {
const data = JSON.parse(message);
// Extract room ID from channel name
const roomId = channel.replace('room:', '');
// Broadcast to all local clients in this room
manager.broadcastToRoom(roomId, data.payload, data.excludeClientId);
});
// WebSocket connection handler
wss.on('connection', (ws, req) => {
const clientId = generateClientId();
const userId = extractUserIdFromRequest(req);
manager.addConnection(clientId, ws, userId);
ws.send(JSON.stringify({
type: 'connected',
clientId,
serverId: process.env.SERVER_ID
}));
ws.on('message', (rawMessage) => {
try {
const message = JSON.parse(rawMessage);
switch (message.type) {
case 'join':
handleJoinRoom(clientId, message.roomId);
break;
case 'leave':
handleLeaveRoom(clientId, message.roomId);
break;
case 'message':
handleRoomMessage(clientId, message.roomId, message.data);
break;
case 'private':
handlePrivateMessage(clientId, message.targetUserId, message.data);
break;
default:
ws.send(JSON.stringify({ type: 'error', message: 'Unknown message type' }));
}
} catch (error) {
console.error('Message handling error:', error);
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' }));
}
});
ws.on('close', () => {
manager.removeConnection(clientId);
console.log(`Client ${clientId} disconnected`);
});
ws.on('error', (error) => {
console.error(`WebSocket error for ${clientId}:`, error);
});
});
function handleJoinRoom(clientId, roomId) {
if (manager.joinRoom(clientId, roomId)) {
// Notify room members
const joinMessage = {
type: 'user-joined',
roomId,
clientId,
timestamp: Date.now()
};
// Publish to Redis for other servers
publisher.publish(`room:${roomId}`, JSON.stringify({
payload: joinMessage,
excludeClientId: clientId
}));
// Broadcast locally
manager.broadcastToRoom(roomId, joinMessage, clientId);
// Confirm to joining client
manager.sendToClient(clientId, {
type: 'joined',
roomId,
timestamp: Date.now()
});
}
}
function handleLeaveRoom(clientId, roomId) {
manager.leaveRoom(clientId, roomId);
// Notify room members
const leaveMessage = {
type: 'user-left',
roomId,
clientId,
timestamp: Date.now()
};
publisher.publish(`room:${roomId}`, JSON.stringify({
payload: leaveMessage
}));
manager.broadcastToRoom(roomId, leaveMessage);
}
function handleRoomMessage(clientId, roomId, data) {
const message = {
type: 'room-message',
roomId,
from: clientId,
data,
timestamp: Date.now()
};
// Publish to Redis for distribution across all servers
publisher.publish(`room:${roomId}`, JSON.stringify({
payload: message,
excludeClientId: clientId
}));
// Broadcast to local clients (excluding sender)
manager.broadcastToRoom(roomId, message, clientId);
// Send confirmation to sender
manager.sendToClient(clientId, {
type: 'message-sent',
roomId,
timestamp: Date.now()
});
}
function handlePrivateMessage(fromClientId, targetUserId, data) {
// Private messages use user-specific channels
const message = {
type: 'private-message',
from: fromClientId,
data,
timestamp: Date.now()
};
// Publish to user-specific channel
publisher.publish(`user:${targetUserId}`, JSON.stringify({
payload: message
}));
}
function generateClientId() {
return `${process.env.SERVER_ID}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
function extractUserIdFromRequest(req) {
// Extract from query params, headers, or JWT
const url = new URL(req.url, `http://${req.headers.host}`);
return url.searchParams.get('userId') || 'anonymous';
}
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`WebSocket server listening on port ${PORT}`);
});
Mermaid Diagram:
flowchart LR
subgraph Server 1
C1[Client A<br/>Subscribed: room1]
C2[Client B<br/>Subscribed: room1, room2]
end
subgraph Server 2
C3[Client C<br/>Subscribed: room2]
C4[Client D<br/>Subscribed: room1]
end
S1[WS Server 1]
S2[WS Server 2]
BROKER{Redis Pub/Sub<br/>Broker}
C1 & C2 --> S1
C3 & C4 --> S2
S1 -->|Publish to room1| BROKER
S1 -->|Subscribe to room1, room2| BROKER
S2 -->|Publish to room1, room2| BROKER
S2 -->|Subscribe to room1, room2| BROKER
BROKER -->|Message from room1| S1
BROKER -->|Message from room1| S2
BROKER -->|Message from room2| S1
BROKER -->|Message from room2| S2
S1 -.->|Broadcast locally| C1 & C2
S2 -.->|Broadcast locally| C3 & C4
style BROKER fill:#ff6b6b
style S1 fill:#4ecdc4
style S2 fill:#4ecdc4
References:
↑ Back to topConnection Lifecycle
How do you establish a WebSocket connection in JavaScript?
The 30-Second Answer: I create a WebSocket connection by instantiating a new WebSocket object with a ws:// or wss:// URL, then attach event listeners for open, message, error, and close events. The connection is established automatically when the WebSocket object is created.
The 2-Minute Answer (If They Want More): Establishing a WebSocket connection is straightforward but requires proper event handling for production use. You instantiate the WebSocket with a URL scheme of ws:// for unencrypted or wss:// for encrypted connections (similar to HTTP vs HTTPS). The browser immediately begins the connection handshake.
The critical part is setting up event handlers before the connection completes. You need handlers for the open event (connection established), message event (receiving data), error event (connection problems), and close event (connection terminated). It's best practice to set these up immediately after creating the WebSocket instance.
For production applications, I always implement reconnection logic, exponential backoff for failed connections, and proper error handling. You should also validate the WebSocket URL and consider connection timeouts. Modern applications often wrap WebSocket creation in a class or factory function to handle these concerns consistently.
WebSocket connections can optionally include subprotocols in the second parameter, which allows the client and server to agree on a specific communication protocol on top of WebSocket (like STOMP or MQTT).
Code Example:
// Basic WebSocket connection
const socket = new WebSocket('wss://api.example.com/socket');
// Set up event handlers immediately
socket.addEventListener('open', (event) => {
console.log('WebSocket connected');
// Send initial message after connection
socket.send(JSON.stringify({ type: 'auth', token: 'xyz' }));
});
socket.addEventListener('message', (event) => {
console.log('Received:', event.data);
const data = JSON.parse(event.data);
// Handle incoming messages
});
socket.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
});
socket.addEventListener('close', (event) => {
console.log('WebSocket closed:', event.code, event.reason);
});
// Production example with subprotocol and reconnection
class WebSocketClient {
constructor(url, protocols = []) {
this.url = url;
this.protocols = protocols;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.connect();
}
connect() {
try {
this.socket = new WebSocket(this.url, this.protocols);
this.socket.onopen = (event) => {
console.log('Connected to WebSocket server');
this.reconnectAttempts = 0; // Reset on successful connection
this.onOpen?.(event);
};
this.socket.onmessage = (event) => {
this.onMessage?.(event);
};
this.socket.onerror = (event) => {
console.error('WebSocket error occurred');
this.onError?.(event);
};
this.socket.onclose = (event) => {
console.log('WebSocket closed');
this.onClose?.(event);
this.handleReconnect();
};
} catch (error) {
console.error('Failed to create WebSocket:', error);
this.handleReconnect();
}
}
handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => this.connect(), delay);
} else {
console.error('Max reconnection attempts reached');
}
}
send(data) {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(data);
} else {
console.warn('WebSocket is not open. Current state:', this.socket.readyState);
}
}
close(code = 1000, reason = 'Client closing') {
this.maxReconnectAttempts = 0; // Prevent reconnection
this.socket.close(code, reason);
}
}
// Usage
const client = new WebSocketClient('wss://api.example.com/socket', ['json']);
client.onMessage = (event) => {
console.log('Got message:', event.data);
};
References:
↑ Back to top