1. Introduction âĄ
Traditional HTTP follows a request-response model â the client always initiates. WebSockets flip this by opening a persistent, bidirectional connection, enabling true real-time features like live chat, notifications, and multiplayer games.
2. What are WebSockets? đ
A WebSocket is a full-duplex communication channel over a single TCP connection. Once established, either side can send messages at any time, without waiting for a request.
- Persistent connection â no repeated handshakes per message.
- Full-duplex â client and server can send simultaneously.
- Low overhead per message compared to repeated HTTP requests.
3. HTTP vs WebSockets đ
| Aspect | HTTP | WebSockets |
|---|---|---|
| Connection | New per request (or Keep-Alive reuse) | Single persistent connection |
| Direction | Client-initiated only | Bidirectional, either side |
| Overhead | Headers on every request | Minimal framing after handshake |
| Use case | Traditional request/response | Real-time, low-latency updates |
4. WebSocket Protocol đ
A WebSocket connection begins as a regular HTTP request with an Upgrade: websocket header. If the server agrees, it responds with 101 Switching Protocols, and the TCP connection is repurposed for the ws:// (or wss:// for encrypted) protocol.
5. Creating a WebSocket Server đĨī¸
Node has no built-in WebSocket module â the ws package is the most widely used lightweight implementation.
ws-server.js
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (socket) => {
console.log('Client connected');
socket.send('Welcome!');
});6. Creating a WebSocket Client đģ
ws-client.js
const socket = new WebSocket('ws://localhost:8080');
socket.addEventListener('open', () => {
console.log('Connected to server');
socket.send('Hello, server!');
});
socket.addEventListener('message', (event) => {
console.log('Received:', event.data);
});Information
7. Connection Lifecycle đ
connection-lifecycle.js
wss.on('connection', (socket) => {
console.log('open');
socket.on('message', (data) => console.log('message:', data.toString()));
socket.on('close', (code, reason) => console.log('closed', code, reason.toString()));
socket.on('error', (err) => console.error('error:', err.message));
});8. Sending Messages đ¤
sending-messages.js
socket.send('plain text message');
socket.send(JSON.stringify({ type: 'chat', text: 'Hello!' }));9. Receiving Messages đĨ
receiving-messages.js
socket.on('message', (data) => {
const parsed = JSON.parse(data.toString());
console.log(parsed.type, parsed.text);
});Tip
10. Broadcasting Messages đĸ
Broadcasting means sending a message to every connected client, typically by iterating over the server's client set.
broadcast.js
function broadcast(wss, message) {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}11. Rooms & Channels đĒ
Rooms (or channels) group clients so messages can be scoped to a subset of connections â essential for multi-conversation chat apps or multiplayer game lobbies.
rooms-example.js
const rooms = new Map(); // roomName -> Set of sockets
function joinRoom(roomName, socket) {
if (!rooms.has(roomName)) rooms.set(roomName, new Set());
rooms.get(roomName).add(socket);
}
function broadcastToRoom(roomName, message) {
const members = rooms.get(roomName) || [];
for (const client of members) {
if (client.readyState === WebSocket.OPEN) client.send(message);
}
}12. Heartbeats & Ping/Pong đ
Since dropped connections aren't always detected immediately, servers use ping/pong frames to periodically verify a client is still alive.
heartbeat.js
wss.on('connection', (socket) => {
socket.isAlive = true;
socket.on('pong', () => (socket.isAlive = true));
});
setInterval(() => {
wss.clients.forEach((socket) => {
if (!socket.isAlive) return socket.terminate();
socket.isAlive = false;
socket.ping();
});
}, 30000);13. Reconnection Strategies đ
Network blips happen â clients should implement automatic reconnection, usually with exponential backoff to avoid hammering the server.
reconnection.js
function connectWithRetry(url, attempt = 0) {
const socket = new WebSocket(url);
socket.addEventListener('close', () => {
const delay = Math.min(1000 * 2 ** attempt, 30000);
setTimeout(() => connectWithRetry(url, attempt + 1), delay);
});
return socket;
}14. Authentication đ
Since the WebSocket handshake is a regular HTTP request, authentication tokens are commonly passed via a query parameter or Authorization header before the upgrade completes.
ws-auth.js
const wss = new WebSocketServer({ noServer: true });
server.on('upgrade', (req, socket, head) => {
const token = new URL(req.url, 'http://localhost').searchParams.get('token');
if (!isValidToken(token)) {
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});15. Authorization đ
Beyond confirming who a user is, authorization determines what they can do â which rooms they can join, which messages they're allowed to send or receive.
ws-authorization.js
socket.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === 'joinRoom' && !userCanJoin(socket.userId, msg.room)) {
socket.send(JSON.stringify({ error: 'Not authorized for this room' }));
return;
}
joinRoom(msg.room, socket);
});16. Binary Data đž
WebSockets support both text and binary frames â useful for transmitting images, audio chunks, or compact binary protocols.
binary-data.js
socket.on('message', (data, isBinary) => {
if (isBinary) {
console.log('Received binary data:', data.length, 'bytes');
} else {
console.log('Received text:', data.toString());
}
});
socket.send(Buffer.from([1, 2, 3, 4]));17. Socket.IO Overview đ§Š
Socket.IO is a popular library built on top of WebSockets (with automatic fallback to HTTP long-polling), adding rooms, reconnection, and acknowledgments out of the box.
socketio-server.js
const { Server } = require('socket.io');
const io = new Server(3000);
io.on('connection', (socket) => {
socket.join('room1');
socket.on('chatMessage', (msg) => {
io.to('room1').emit('chatMessage', msg);
});
});Information
18. Server-Sent Events (SSE) đļ
SSE provides a one-way (server-to-client) real-time channel over plain HTTP â simpler than WebSockets when the client never needs to send data back.
sse-server.js
http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const interval = setInterval(() => {
res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
}, 1000);
req.on('close', () => clearInterval(interval));
}).listen(3000);Tip
19. Real-Time Notifications đ
A typical notification system pushes events to specific connected users, often keyed by user ID rather than a shared room.
notifications.js
const userSockets = new Map(); // userId -> socket
function notifyUser(userId, payload) {
const socket = userSockets.get(userId);
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'notification', ...payload }));
}
}20. Live Chat Applications đŦ
live-chat.js
wss.on('connection', (socket, req) => {
socket.on('message', (data) => {
const { room, text, user } = JSON.parse(data.toString());
broadcastToRoom(room, JSON.stringify({ user, text, ts: Date.now() }));
});
});21. Multiplayer Applications đŽ
Real-time multiplayer games broadcast frequent, small state updates (positions, actions) â often over UDP-like protocols in native games, but WebSockets work well for browser-based games with moderate update rates.
multiplayer-state.js
setInterval(() => {
const state = getWorldState();
broadcast(wss, JSON.stringify({ type: 'stateUpdate', state }));
}, 50); // ~20 updates per second22. Performance Optimization âĄ
- Batch frequent small updates instead of sending a message per tiny change.
- Use binary formats (like MessagePack) over JSON for high-frequency data.
- Scale across processes with a shared pub/sub layer (e.g. Redis) so broadcasts reach clients connected to different server instances.
- Tune heartbeat intervals to balance responsiveness against unnecessary network traffic.
23. Security Best Practices đĄī¸
- Always use wss:// (encrypted) in production, never plain ws://.
- Validate the Origin header during the handshake to prevent unauthorized cross-site connections.
- Authenticate before completing the upgrade, not after the connection is already open.
- Rate-limit incoming messages per connection to prevent abuse or flooding.
- Validate and sanitize all message payloads â never trust client-supplied data.
24. Common Mistakes â ī¸
- Forgetting to check readyState before calling .send(), causing errors on closed sockets.
- Not implementing heartbeats, leaving dead connections lingering indefinitely.
- Storing room/user membership only in memory on a single server instance, breaking under horizontal scaling.
- Trusting client-sent user IDs instead of deriving identity from an authenticated session.
- Not handling the 'error' event, leading to unhandled exceptions on connection issues.