JavaScript WebSockets — Real-Time Communication, Reconnection, and Message Protocols
In this tutorial, you will learn about JavaScript WebSockets. We cover key concepts, practical examples, and best practices to help you master this topic.
WebSockets provide full-duplex communication over a single TCP connection, enabling real-time features like live dashboards, chat applications, and collaborative editing. Unlike HTTP polling, WebSockets maintain a persistent connection with minimal overhead.
DodaTech uses WebSockets for real-time scan status updates, live security alerts, and streaming log data to the dashboard.
What You'll Learn
- Websocket client API
- Message protocols (JSON, binary, custom)
- Automatic reconnection with exponential backoff
- Heartbeat/ping-pong
- Binary message handling
- Integration with frameworks
- Performance and scaling considerations
Basic WebSocket Client
const socket = new WebSocket("wss://api.example.com/ws");
// Connection opened
socket.addEventListener("open", (event) => {
console.log("Connected to server");
// Send a message
socket.send(JSON.stringify({
type: "subscribe",
channel: "scans"
}));
});
// Listen for messages
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
console.log("Received:", data);
switch (data.type) {
case "scan_update":
updateScanStatus(data.payload);
break;
case "alert":
showNotification(data.payload);
break;
case "error":
handleError(data.payload);
break;
}
});
// Handle errors
socket.addEventListener("error", (event) => {
console.error("WebSocket error:", event);
});
// Connection closed
socket.addEventListener("close", (event) => {
console.log(`Disconnected: code=${event.code}, reason=${event.reason}`);
// event.code: 1000 (normal), 1006 (abnormal), etc.
});
Reconnection with Backoff
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.reconnectInterval = options.reconnectInterval || 1000;
this.maxReconnectInterval = options.maxReconnectInterval || 30000;
this.maxRetries = options.maxRetries || Infinity;
this.messageQueue = [];
this.retryCount = 0;
this.intentionalClose = false;
this.connect();
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = (event) => {
console.log("WebSocket connected");
this.retryCount = 0;
this.onConnect?.(event);
// Flush queued messages
while (this.messageQueue.length) {
this.socket.send(this.messageQueue.shift());
}
};
this.socket.onmessage = (event) => {
this.onMessage?.(event);
};
this.socket.onerror = (event) => {
console.error("WebSocket error");
this.onError?.(event);
};
this.socket.onclose = (event) => {
if (!this.intentionalClose) {
console.log(`Connection lost (attempt ${this.retryCount + 1})`);
this.scheduleReconnect();
}
this.onClose?.(event);
};
}
scheduleReconnect() {
if (this.retryCount >= this.maxRetries) {
console.error("Max retries reached");
this.onMaxRetries?.();
return;
}
// Exponential backoff with jitter
const baseDelay = Math.min(
this.reconnectInterval * Math.pow(2, this.retryCount),
this.maxReconnectInterval
);
const jitter = Math.random() * 1000;
const delay = baseDelay + jitter;
console.log(`Reconnecting in ${Math.round(delay)}ms...`);
this.retryCount++;
setTimeout(() => this.connect(), delay);
}
send(data) {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(data);
} else {
this.messageQueue.push(data);
}
}
close(code = 1000, reason = "Client closing") {
this.intentionalClose = true;
this.socket?.close(code, reason);
}
// Callbacks
onConnect(event) {}
onMessage(event) {}
onError(event) {}
onClose(event) {}
onMaxRetries() {}
}
Heartbeat (Ping/Pong)
class HeartbeatWebSocket extends ReconnectingWebSocket {
constructor(url, options = {}) {
super(url, options);
this.pingInterval = options.pingInterval || 30000; // 30s
this.pongTimeout = options.pongTimeout || 10000; // 10s
this.pingTimer = null;
this.pongTimer = null;
this.lastPong = Date.now();
}
connect() {
super.connect();
this.startHeartbeat();
}
startHeartbeat() {
this.pingTimer = setInterval(() => {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({ type: "ping" }));
// Wait for pong
this.pongTimer = setTimeout(() => {
const elapsed = Date.now() - this.lastPong;
if (elapsed > this.pongTimeout) {
console.warn("Pong timeout, reconnecting...");
this.socket?.close();
}
}, this.pongTimeout);
}
}, this.pingInterval);
}
handlePong() {
this.lastPong = Date.now();
clearTimeout(this.pongTimer);
}
close(code, reason) {
clearInterval(this.pingTimer);
clearTimeout(this.pongTimer);
super.close(code, reason);
}
}
Message Protocol
// Standard message envelope
class MessageProtocol {
static encode(type, payload, requestId = null) {
return JSON.stringify({
type,
payload,
requestId,
timestamp: Date.now(),
version: 1
});
}
static decode(raw) {
try {
const msg = JSON.parse(raw);
if (!msg.type) throw new Error("Missing type");
return msg;
} catch (err) {
console.error("Invalid message:", err);
return null;
}
}
}
// Request-response pattern over WebSocket
class RpcClient {
constructor(socket) {
this.socket = socket;
this.pending = new Map();
this.requestId = 0;
socket.onMessage = (event) => {
const msg = MessageProtocol.decode(event.data);
if (!msg) return;
if (msg.type === "response" && msg.requestId) {
const { resolve, reject } = this.pending.get(msg.requestId) || {};
if (resolve) {
this.pending.delete(msg.requestId);
if (msg.payload.error) {
reject(new Error(msg.payload.error));
} else {
resolve(msg.payload);
}
}
} else {
this.onMessage?.(msg);
}
};
}
async request(type, payload, timeout = 10000) {
const requestId = ++this.requestId;
const msg = MessageProtocol.encode(type + "_request", payload, requestId);
return new Promise((resolve, reject) => {
this.pending.set(requestId, { resolve, reject });
this.socket.send(msg);
setTimeout(() => {
if (this.pending.has(requestId)) {
this.pending.delete(requestId);
reject(new Error("Request timeout"));
}
}, timeout);
});
}
onMessage(msg) {}
}
Binary Messages
// Sending binary data
const socket = new WebSocket("wss://api.example.com/ws");
socket.binaryType = "arraybuffer"; // or "blob"
// Send binary frame
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setUint32(0, 12345); // sequence number
view.setFloat32(4, 98.6); // value
socket.send(buffer);
// Receiving binary
socket.addEventListener("message", (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new DataView(event.data);
const seq = view.getUint32(0);
const value = view.getFloat32(4);
console.log(`Sequence ${seq}: ${value}`);
} else {
// Text message
const msg = JSON.parse(event.data);
console.log("Text:", msg);
}
});
// Binary protocol: first 4 bytes = message type
// Use for high-throughput data (metrics, logs, binary blobs)
Practice Questions
Implement a WebSocket client that subscribes to multiple channels and routes messages to different handlers.
Build a reconnecting WebSocket with progressive backoff (1s, 2s, 4s, 8s, max 30s) and jitter.
Implement a request-response protocol over WebSocket (correlate requests to responses).
Write a WebSocket Connection Pool that distributes messages across multiple connections.
Implement a binary protocol for streaming time-series data (compact, typed arrays).
Challenge: Live Dashboard Data Stream
Build a WebSocket client for a real-time dashboard that:
- Connects and authenticates (send auth token on open)
- Subscribes to multiple metric channels
- Reconnects with backoff on disconnect
- Buffers messages while disconnected, replays on reconnect
- Renders metrics in real-time (use requestAnimationFrame for batching)
- Handles stale connections (heartbeat/pong timeout)
- Shows connection status indicator
This is essentially DodaTech's live dashboard — showing real-time security scan status across thousands of assets with WebSocket-powered updates.
Real-World Task: Multi-Room Chat Client
Implement a WebSocket-based chat client:
- Connect to a chat server
- Join/leave rooms
- Send messages with typing indicators
- Display read receipts
- Handle reconnection gracefully (preserve room state)
- Sync message history on reconnect (last N messages)
- Offline message queue (store-and-forward)
- Binary messages for audio snippets
This architecture mirrors DodaTech's alerting system, where security events are streamed to different "rooms" (customer channels, severity levels) in real time.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro