// ── Create WebSocket Connection ──
const ws = new WebSocket('wss://example.com/ws');
// ── Connection Lifecycle Events ──
ws.onopen = () => {
console.log('Connected to server');
ws.send(JSON.stringify({ type: 'hello', payload: 'world' }));
};
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(`Disconnected: code=${event.code}, reason=${event.reason}`);
if (!event.wasClean) setTimeout(connect, 3000);
};
// ── Ready States ──
// 0 = CONNECTING, 1 = OPEN, 2 = CLOSING, 3 = CLOSED
if (ws.readyState === WebSocket.OPEN) {
ws.send('Hello!');
}
// ── Close Connection ──
ws.close(1000, 'Goodbye');
// ── Send Binary Data ──
const binary = new ArrayBuffer(8);
const view = new DataView(binary);
view.setFloat64(0, 3.14);
ws.send(binary);
// ── Send Blob ──
const blob = new Blob(['hello'], { type: 'text/plain' });
ws.send(blob);// ── Reconnecting WebSocket with Exponential Backoff ──
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.reconnectInterval = options.reconnectInterval || 1000;
this.maxRetries = options.maxRetries || 10;
this.retries = 0;
this.onopen = options.onopen;
this.onmessage = options.onmessage;
this.onclose = options.onclose;
this.onerror = options.onerror;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.retries = 0;
this.onopen?.();
};
this.ws.onmessage = (e) => this.onmessage?.(e);
this.ws.onclose = (e) => {
this.onclose?.(e);
if (this.retries < this.maxRetries) {
const delay = this.reconnectInterval * Math.pow(2, this.retries);
setTimeout(() => { this.retries++; this.connect(); }, delay);
}
};
this.ws.onerror = (e) => this.onerror?.(e);
}
send(data) { if (this.ws.readyState === 1) this.ws.send(data); }
close() { this.ws.close(); }
}
// Usage
const ws = new ReconnectingWebSocket('wss://api.example.com/ws', {
onopen: () => console.log('Connected!'),
onmessage: (e) => console.log('Message:', e.data),
onclose: (e) => console.log('Closed:', e.code),
reconnectInterval: 1000,
maxRetries: 10,
});Feature WebSocket HTTP Communication Bidirectional Request-Response Connection Persistent (TCP) Short-lived Overhead Low (2 bytes header) High (200+ bytes) Latency Very low Higher per request State Connection-oriented Stateless Scaling Sticky sessions needed Load balancer friendly Protocol ws:// or wss:// http:// or https:// Use Cases Chat, gaming, live feeds CRUD, REST APIs
Code Meaning 1000 Normal closure 1001 Going away (tab close) 1003 Unsupported data type 1005 No status code received 1006 Abnormal closure (no close frame) 1007 Invalid frame payload data 1008 Policy violation 1009 Message too big 1010 Extension expected 1011 Internal server error 1012 Server restarting 1013 Try again later 4000-4999 Application-defined
💡 Always use wss:// (WebSocket Secure) in production. Unencrypted ws:// connections are vulnerable to man-in-the-middle attacks. Use exponential backoff for reconnection to avoid thundering herd problems.
// ── ws Library: Basic Server ──
const { WebSocketServer } = require('ws');
const http = require('http');
const server = http.createServer();
const wss = new WebSocketServer({ server, path: '/ws' });
wss.on('connection', (ws, req) => {
const clientIp = req.socket.remoteAddress;
console.log('New connection:', clientIp);
ws.on('message', (data) => {
const msg = data.toString();
console.log('Received:', msg);
// Echo back
ws.send(`Server echo: ${msg}`);
});
ws.on('close', (code, reason) => {
console.log('Disconnected:', code, reason.toString());
});
ws.on('ping', () => {
console.log('Ping received');
});
// Send welcome message
ws.send(JSON.stringify({ type: 'welcome', id: generateId() }));
});
// ── Broadcast to all clients ──
function broadcast(data) {
const msg = typeof data === 'string' ? data : JSON.stringify(data);
wss.clients.forEach((client) => {
if (client.readyState === 1) client.send(msg);
});
}
// ── Heartbeat: detect dead connections ──
const heartbeat = setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
wss.on('close', () => clearInterval(heartbeat));
server.listen(8080, () => console.log('WS server on :8080'));// ── Authentication on Connection ──
const { WebSocketServer } = require('ws');
const jwt = require('jsonwebtoken');
const wss = new WebSocketServer({
verifyClient: (info, cb) => {
const url = new URL(info.req.url, 'http://localhost');
const token = url.searchParams.get('token');
if (!token) return cb(false, 401, 'Missing token');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
info.req.user = decoded; // attach to request
cb(true);
} catch (err) {
cb(false, 401, 'Invalid token');
}
},
});
wss.on('connection', (ws, req) => {
const user = req.user;
console.log(`Authenticated: ${user.sub}`);
ws.send(JSON.stringify({ type: 'auth_ok', user }));
});
// ── First-message auth (alternative) ──
wss.on('connection', (ws) => {
ws.authenticated = false;
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.type === 'auth' && !ws.authenticated) {
try {
const decoded = jwt.verify(msg.token, process.env.JWT_SECRET);
ws.authenticated = true;
ws.user = decoded;
ws.send(JSON.stringify({ type: 'auth_ok' }));
} catch {
ws.close(1008, 'Authentication failed');
}
return;
}
if (!ws.authenticated) {
ws.close(1008, 'Not authenticated');
return;
}
// Process normal messages...
});
});⚠️ Always implement heartbeat/keepalive. Use ping/pong frames to detect dead connections. Network devices (NATs, proxies) may silently drop idle connections. A 30-second interval is a common default.
server/socketio-server.js
// ── Socket.IO Server (Node.js) ──
const { Server } = require('socket.io');
const http = require('http');
const httpServer = http.createServer();
const io = new Server(httpServer, {
cors: { origin: '*', methods: ['GET', 'POST'] },
pingInterval: 10000,
pingTimeout: 5000,
});
// ── Middleware: Authentication ──
io.use((socket, next) => {
const token = socket.handshake.auth.token;
try {
socket.user = jwt.verify(token, SECRET);
next();
} catch (err) {
next(new Error('Authentication failed'));
}
});
io.on('connection', (socket) => {
console.log('Connected:', socket.id, socket.user?.sub);
// ── Join / Leave Rooms ──
socket.on('room:join', (roomId) => {
socket.join(roomId);
socket.to(roomId).emit('user:joined', { userId: socket.user.sub });
console.log(`${socket.id} joined ${roomId}`);
});
socket.on('room:leave', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('user:left', { userId: socket.user.sub });
});
// ── Chat Messages ──
socket.on('message:send', async ({ roomId, content }) => {
const message = { id: Date.now(), userId: socket.user.sub, content, ts: Date.now() };
// Save to DB...
io.to(roomId).emit('message:new', message);
});
// ── Private Message ──
socket.on('message:private', ({ to, content }) => {
io.to(to).emit('message:private', {
from: socket.id, fromUser: socket.user.sub, content, ts: Date.now()
});
});
// ── Typing Indicator ──
socket.on('typing:start', (roomId) => {
socket.to(roomId).emit('user:typing', { userId: socket.user.sub });
});
socket.on('disconnect', (reason) => {
console.log('Disconnected:', socket.id, reason);
});
});
// ── Emit Patterns ──
io.emit('global', data); // ALL connected clients
socket.emit('event', data); // one specific client
socket.to('room').emit('event', data); // room, excluding sender
io.to('room').emit('event', data); // room, including sender
io.to('room1').to('room2').emit('event', data); // multiple rooms
httpServer.listen(3000);client/socketio-client.js
// ── Socket.IO Client (Browser) ──
import { io } from 'socket.io-client';
const socket = io('https://api.example.com', {
auth: { token: localStorage.getItem('jwt') },
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 10,
timeout: 10000,
});
socket.on('connect', () => {
console.log('Connected:', socket.id);
socket.emit('room:join', 'chat-room-123');
});
socket.on('message:new', (msg) => {
appendMessage(msg);
});
socket.on('user:typing', ({ userId }) => {
showTypingIndicator(userId);
});
socket.on('connect_error', (err) => {
console.error('Connection error:', err.message);
});
socket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
});
// ── Send message with ACK ──
socket.emit('message:send', { roomId: '123', content: 'Hello!' }, (ack) => {
if (ack?.status === 'ok') {
console.log('Message delivered');
}
});
// ── Disconnect ──
socket.disconnect();Feature Socket.IO Raw WebSocket Auto-reconnect Yes Manual Rooms/Namespace Built-in Manual Fallback transport Long-polling No fallback Broadcasting Built-in Manual Acknowledgement Callback support Manual Binary support Yes Yes Multiplexing Namespaces Manual Overhead Higher (metadata) Minimal
Concept Description Namespace Endpoint separation: /chat, /notifications Default NS / (always connected) Room Channel for broadcasting within namespace socket.join(room) Join a room socket.leave(room) Leave a room socket.rooms Set of joined room IDs io.to(room) Emit to room (all sockets) socket.to(room) Emit to room (exclude sender)
// ── 1. Message Protocol Schema ──
// Client -> Server
{ "type": "chat:message", "roomId": "abc", "content": "Hello" }
{ "type": "typing:start", "roomId": "abc" }
{ "type": "auth", "token": "jwt-string" }
// Server -> Client
{ "type": "chat:message", "id": 123, "userId": "u1", "content": "Hello", "ts": 1700000000 }
{ "type": "user:joined", "userId": "u2" }
{ "type": "presence:update", "userId": "u1", "status": "online" }
// ── 2. Message Queue with Acknowledgement ──
const pendingMessages = new Map();
function sendWithAck(socket, event, data, timeout = 5000) {
const id = crypto.randomUUID();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pendingMessages.delete(id);
reject(new Error('Acknowledgement timeout'));
}, timeout);
pendingMessages.set(id, { resolve, reject, timer });
socket.send(JSON.stringify({ id, event, data }));
});
}
socket.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.ack) {
const pending = pendingMessages.get(msg.ack);
if (pending) {
pending.resolve(msg.data);
clearTimeout(pending.timer);
pendingMessages.delete(msg.ack);
}
}
});
// ── 3. Rate Limiting Per Connection ──
const rateLimits = new Map();
function checkRateLimit(socket, maxPerSec = 10) {
const now = Date.now();
const record = rateLimits.get(socket.id) || { count: 0, resetAt: now + 1000 };
if (now > record.resetAt) {
record.count = 0;
record.resetAt = now + 1000;
}
record.count++;
rateLimits.set(socket.id, record);
if (record.count > maxPerSec) {
socket.send(JSON.stringify({ type: 'error', message: 'Rate limit exceeded' }));
return false;
}
return true;
}
// ── 4. Presence Tracking ──
const onlineUsers = new Map(); // userId -> Set of socketIds
socket.on('disconnect', () => {
onlineUsers.forEach((sockets, userId) => {
sockets.delete(socket.id);
if (sockets.size === 0) {
onlineUsers.delete(userId);
io.emit('presence:update', { userId, status: 'offline' });
}
});
});// ── Error Handling Best Practices ──
wss.on('connection', (ws) => {
ws.on('message', (data) => {
try {
const msg = JSON.parse(data);
if (!msg.type) {
ws.send(JSON.stringify({
type: 'error',
code: 'INVALID_MESSAGE',
message: 'Message must have a type field',
}));
return;
}
handleMessage(ws, msg);
} catch (err) {
ws.send(JSON.stringify({
type: 'error',
code: 'PARSE_ERROR',
message: 'Invalid JSON',
}));
}
});
ws.on('error', (err) => {
console.error('WebSocket error:', err.message);
});
ws.on('close', (code) => {
if (code === 1006) {
console.warn('Abnormal closure — possible network issue');
}
cleanupConnection(ws);
});
});
// ── Graceful Server Shutdown ──
function gracefulShutdown() {
console.log('Shutting down...');
wss.clients.forEach((ws) => {
ws.close(1012, 'Server shutting down');
});
wss.close(() => {
httpServer.close(() => {
console.log('Server closed');
process.exit(0);
});
});
}
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);Practice Reason Use wss:// (TLS) Encrypt all traffic Authenticate connections Prevent unauthorized access Implement heartbeat Detect dead connections Rate limit per socket Prevent abuse/DoS Validate all messages Prevent injection attacks Graceful shutdown Close connections cleanly Use message protocol Structured, typed messages Monitor connection count Track server health Set max message size Prevent memory exhaustion Use namespaces/rooms Organize communication
Feature WebSocket SSE Long Polling Direction Bidirectional Server→Client Request-Response Binary data Yes No Yes Auto-reconnect Manual Built-in N/A Browser support All modern All except IE All Complexity Medium Low Low Overhead Low (2-14 bytes) HTTP headers HTTP per poll Protocol ws:// / wss:// HTTP HTTP Best for Chat, gaming Live feeds, notifications Fallback
Q: When to use WebSocket vs REST? WebSocket: real-time (chat, gaming, live feeds, collaborative editing), bidirectional, low latency, frequent updates. REST: CRUD operations, infrequent requests, stateless, cacheable responses, simple APIs. Use WebSocket when you need instant server-to-client or bidirectional communication.
Q: How does WebSocket handle reconnection? Implement exponential backoff: start at 1s delay, double each retry up to 30s max. Track connection health with ping/pong frames. On reconnect, send last-seen sequence number to replay missed messages. Always handle race conditions from multiple reconnection attempts.
Q: How do you secure WebSocket connections? Use wss:// (TLS), authenticate via token (query param, first message, or subprotocol), validate on server before upgrading, use rate limiting per connection, sanitize all input messages, implement origin checking to prevent CSRF, and use heartbeat to detect dead connections.
Q: How does WebSocket scaling work? WebSocket connections are stateful and sticky to one server. Solutions: (1) Redis Pub/Sub to broadcast across servers, (2) Socket.IO Redis adapter for rooms/namespace sync, (3) Sticky sessions at load balancer level (ip_hash). Each server handles its own connections; messages broadcast via broker.
Q: What is the WebSocket handshake? Client sends HTTP GET with Upgrade: websocket and Connection: Upgrade headers. Server responds with 101 Switching Protocols. After handshake, the protocol switches from HTTP to WebSocket. The Sec-WebSocket-Key header is used for verification.
Q: What is the difference between WebSocket and SSE? WebSocket: full-duplex (bidirectional), binary support, lower overhead after handshake. SSE (Server-Sent Events): server-to-client only, auto-reconnect built-in, text-only, works over standard HTTP, simpler API. Use SSE for notifications/feeds, WebSocket for chat/gaming.
Q: How do you handle message ordering? Assign a sequence number to each message on the server. On reconnect, the client sends the last received sequence number, and the server replays any missed messages. Store recent messages in a buffer (e.g., last 100 per room). For strict ordering, process messages sequentially per room.
Q: How much memory does a WebSocket connection use? Approximately 10-100KB per connection in Node.js, depending on buffer sizes. With 100K connections, expect 1-10GB. Use wss options to limit maxPayload (default 100MB), enable permessage-deflate compression, and monitor memory usage. Consider multiple server instances for scale.
💡 Top WebSocket interview topics: lifecycle (open/message/close/error), ready states, close codes, reconnection with exponential backoff, authentication patterns, scaling with Redis, Socket.IO features (rooms, namespaces, acks), WebSocket vs SSE vs Long Polling, and message protocol design.