WebSockets provide full-duplex communication between browser and server — the server can push data to the client at any time, without the client polling. Firefox has excellent WebSocket support, and DevTools has a dedicated WebSocket message inspector. This guide covers the API, patterns, and debugging.
The WebSocket API
// Connect
const ws = new WebSocket('wss://api.example.com/ws');
// wss:// = WebSocket Secure (TLS); ws:// = unencrypted
// Connection events
ws.onopen = () => {
console.log('Connected');
ws.send('Hello, server!');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = (event) => {
console.log('Closed:', event.code, event.reason);
// Reconnect if unexpected
if (event.code !== 1000) {
reconnect();
}
};
// Send
ws.send('text message');
ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
ws.send(arrayBuffer); // Binary
ws.send(blob); // Blob
// Close
ws.close(1000, 'Normal closure');
// Codes: 1000=normal, 1001=going away, 1002=protocol error, 1003=unsupported data
WebSocket State
ws.readyState // 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
// Constants
WebSocket.CONNECTING // 0
WebSocket.OPEN // 1
WebSocket.CLOSING // 2
WebSocket.CLOSED // 3
Message Protocol Design
A common pattern: message envelope with type field:
// Client sends
ws.send(JSON.stringify({
type: 'subscribe',
channel: 'stock-prices',
symbols: ['AAPL', 'GOOGL'],
}));
// Server sends
// { type: 'price-update', symbol: 'AAPL', price: 189.45, timestamp: 1729080000 }
// { type: 'error', message: 'Symbol not found', symbol: 'XXXX' }
// { type: 'ping' }
// Dispatch by type
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'price-update':
updatePriceDisplay(msg);
break;
case 'error':
showError(msg.message);
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong' }));
break;
}
};
Reconnection with Exponential Backoff
WebSocket connections drop. Reconnect automatically:
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.options = { maxDelay: 30000, factor: 1.5, ...options };
this.delay = 1000;
this.listeners = {};
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.delay = 1000; // Reset on success
this.emit('open');
};
this.ws.onmessage = (e) => this.emit('message', e);
this.ws.onerror = (e) => this.emit('error', e);
this.ws.onclose = (e) => {
this.emit('close', e);
if (e.code !== 1000) {
setTimeout(() => this.connect(), this.delay);
this.delay = Math.min(this.delay * this.options.factor, this.options.maxDelay);
}
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(typeof data === 'string' ? data : JSON.stringify(data));
}
}
on(event, handler) {
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push(handler);
}
emit(event, data) {
(this.listeners[event] || []).forEach(fn => fn(data));
}
close() {
this.ws.close(1000, 'Client closed');
}
}
const ws = new ReconnectingWebSocket('wss://api.example.com/ws');
ws.on('message', (e) => {
const data = JSON.parse(e.data);
console.log(data);
});
ws.on('open', () => ws.send({ type: 'subscribe', channel: 'updates' }));
Binary Messages
WebSockets support binary data efficiently:
// Receive binary
ws.binaryType = 'arraybuffer'; // or 'blob'
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new DataView(event.data);
const type = view.getUint8(0); // First byte: message type
const length = view.getUint16(1); // Bytes 1-2: payload length
// Process binary payload...
} else {
// Text message
const data = JSON.parse(event.data);
}
};
// Send binary
const buffer = new ArrayBuffer(5);
const view = new DataView(buffer);
view.setUint8(0, 1); // Type: 1
view.setUint16(1, 100); // Value: 100
ws.send(buffer);
Binary is useful for: game state synchronization, audio/video streaming, high-frequency sensor data.
WebSocket Heartbeat
Keep the connection alive and detect dropped connections:
class HeartbeatWebSocket {
constructor(url) {
this.url = url;
this.heartbeatInterval = 30000;
this.pongTimeout = 5000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => this.startHeartbeat();
this.ws.onmessage = (e) => {
if (e.data === 'pong') this.receivedPong = true;
else this.onMessage(e);
};
this.ws.onclose = () => this.stopHeartbeat();
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
this.receivedPong = false;
this.ws.send('ping');
setTimeout(() => {
if (!this.receivedPong) {
this.ws.close(); // Connection is dead
this.connect(); // Reconnect
}
}, this.pongTimeout);
}, this.heartbeatInterval);
}
stopHeartbeat() {
clearInterval(this.heartbeatTimer);
}
}
Debugging WebSockets in Firefox
Firefox DevTools has first-class WebSocket support:
- Open DevTools → Network tab
- Filter to WS (WebSocket connections)
- Click a WebSocket connection
-
Messages sub-tab shows every message with:
- Direction (▲ sent, ▼ received)
- Timestamp
- Data (expandable for JSON)
- Size
- Click any message to inspect the full payload
You can see the initial HTTP upgrade handshake in the Headers sub-tab.
WebSockets vs. Server-Sent Events vs. Polling
| WebSocket | Server-Sent Events | Long Polling | |
|---|---|---|---|
| Direction | Bidirectional | Server → Client only | Server → Client |
| Protocol | ws:// / wss:// | HTTP | HTTP |
| Complexity | Medium | Low | Low |
| Reconnect | Manual | Automatic | Automatic |
| Firefox support | Full | Full | Full |
| Use when | Chat, games, real-time collaboration | Live feeds, notifications | Simple updates |
Use SSE for one-way server-to-client data (news feeds, stock tickers, progress updates). Use WebSocket for bidirectional communication.
Top comments (1)
One shutdown edge case in
ReconnectingWebSocket: after an abnormal close has queued the retry, callingclose()during the backoff does not cancel that timer, so the wrapper can reconnect after the caller has asked it to stop. I would keep a terminalclosedflag, save the timeout ID, clear it inclose(), and guard both scheduling andconnect(). A useful regression case is: disconnect, callclose()before the backoff expires, then verify that no new socket is created. MDN documents timer cancellation.AI-assisted reply; I work on Lisar Connect.