websocket-realtime
Build WebSocket servers, implement client reconnection with backoff, heartbeat keepalive, SSE, Socket.io rooms, Redis pub/sub scaling, and authentication.
WebSocket and Real-Time Communication
Build production-ready WebSocket servers with reconnection, heartbeat, authentication, and horizontal scaling.
WebSocket Server (Node.js ws)
BAD: No connection lifecycle management.
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
// Broadcast to everyone
wss.clients.forEach((client) => {
client.send(data);
});
});
});
GOOD: Connection tracking, error handling, graceful shutdown.
import WebSocket, { WebSocketServer } from 'ws';
import http from 'http';
interface Client {
ws: WebSocket;
userId: string;
rooms: Set<string>;
isAlive: boolean;
}
const server = http.createServer();
const wss = new WebSocketServer({ server });
const clients = new Map<WebSocket, Client>();
wss.on('connection', (ws, req) => {
const client: Client = {
ws,
userId: '', // Set after auth
rooms: new Set(),
isAlive: true,
};
clients.set(ws, client);
ws.on('pong', () => {
client.isAlive = true;
});
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
handleMessage(client, msg);
} catch (err) {
ws.send(JSON.stringify({ error: 'Invalid message format' }));
}
});
ws.on('error', (err) => {
console.error('WebSocket error:', err);
});
ws.on('close', () => {
clients.delete(ws);
});
ws.send(JSON.stringify({ type: 'connected' }));
});
// Heartbeat interval
const heartbeat = setInterval(() => {
wss.clients.forEach((ws) => {
const client = clients.get(ws);
if (!client) return;
if (!client.isAlive) {
client.rooms.clear();
clients.delete(ws);
return ws.terminate();
}
client.isAlive = false;
ws.ping();
});
}, 30000);
// Graceful shutdown
process.on('SIGTERM', () => {
clearInterval(heartbeat);
wss.clients.forEach((ws) => {
ws.close(1000, 'Server shutting down');
});
server.close(() => {
process.exit(0);
});
});
server.listen(8080);
Client Reconnection with Exponential Backoff
BAD: No reconnection or infinite reconnect spam.
const ws = new WebSocket('ws://localhost:8080');
ws.onclose = () => {
// Lost connection, nothing happens
};
GOOD: Exponential backoff with jitter and max attempts.
class ReconnectingWebSocket {
private ws: WebSocket | null = null;
private url: string;
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private baseDelay = 1000;
private maxDelay = 30000;
private reconnectTimer: NodeJS.Timeout | null = null;
constructor(url: string) {
this.url = url;
this.connect();
}
private connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
this.handleMessage(msg);
};
this.ws.onerror = (err) => {
console.error('WebSocket error:', err);
};
this.ws.onclose = () => {
console.log('Disconnected');
this.scheduleReconnect();
};
}
private scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnect attempts reached');
return;
}
const delay = Math.min(
this.baseDelay * Math.pow(2, this.reconnectAttempts),
this.maxDelay
);
const jitter = delay * 0.3 * Math.random();
const totalDelay = delay + jitter;
this.reconnectAttempts++;
console.log(`Reconnecting in ${Math.round(totalDelay)}ms (attempt ${this.reconnectAttempts})`);
this.reconnectTimer = setTimeout(() => {
this.connect();
}, totalDelay);
}
send(data: any) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
console.warn('WebSocket not open, message queued');
}
}
close() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
this.ws?.close();
}
private handleMessage(msg: any) {
// Application logic
}
}
Authentication on WebSocket Upgrade
BAD: No authentication, anyone can connect.
wss.on('connection', (ws) => {
// Unauthenticated connection
});
GOOD: JWT verification during upgrade handshake.
import jwt from 'jsonwebtoken';
import { parse } from 'url';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret';
wss.on('connection', (ws, req) => {
const { query } = parse(req.url || '', true);
const token = query.token as string;
if (!token) {
ws.close(1008, 'Authentication required');
return;
}
try {
const payload = jwt.verify(token, JWT_SECRET) as { userId: string };
const client: Client = {
ws,
userId: payload.userId,
rooms: new Set(),
isAlive: true,
};
clients.set(ws, client);
ws.send(JSON.stringify({ type: 'authenticated', userId: payload.userId }));
} catch (err) {
ws.close(1008, 'Invalid token');
}
});
Room-Based Broadcasting
BAD: Broadcasting to all clients regardless of relevance.
function broadcast(message: any) {
wss.clients.forEach((client) => {
client.send(JSON.stringify(message));
});
}
GOOD: Room-based message routing.
function joinRoom(client: Client, room: string) {
client.rooms.add(room);
client.ws.send(JSON.stringify({ type: 'joined', room }));
}
function leaveRoom(client: Client, room: string) {
client.rooms.delete(room);
client.ws.send(JSON.stringify({ type: 'left', room }));
}
function broadcastToRoom(room: string, message: any, excludeClient?: Client) {
clients.forEach((client) => {
if (client.rooms.has(room) && client !== excludeClient) {
client.ws.send(JSON.stringify(message));
}
});
}
function handleMessage(client: Client, msg: any) {
switch (msg.type) {
case 'join':
joinRoom(client, msg.room);
break;
case 'leave':
leaveRoom(client, msg.room);
break;
case 'message':
broadcastToRoom(msg.room, {
type: 'message',
from: client.userId,
data: msg.data,
}, client);
break;
}
}
<!-- See references/advanced.md for extended examples -->