WebSockets enable bidirectional, real-time communication between clients and servers. Unlike HTTP's request-response model, WebSockets maintain persistent connections for instant data exchange.
When to Use WebSockets#
Good fit:
- Live chat applications
- Real-time notifications
- Collaborative editing
- Live dashboards
- Gaming
- Financial tickers
Consider alternatives:
- Server-Sent Events (SSE) for one-way updates
- Long polling for simple use cases
- HTTP polling for infrequent updates
Basic Implementation#
Server (Node.js with ws)#
1import { WebSocketServer, WebSocket } from 'ws';
2import { createServer } from 'http';
3
4const server = createServer();
5const wss = new WebSocketServer({ server });
6
7wss.on('connection', (ws: WebSocket) => {
8 console.log('Client connected');
9
10 ws.on('message', (data: Buffer) => {
11 const message = JSON.parse(data.toString());
12 console.log('Received:', message);
13
14 // Echo back
15 ws.send(JSON.stringify({ type: 'echo', data: message }));
16 });
17
18 ws.on('close', () => {
19 console.log('Client disconnected');
20 });
21
22 ws.on('error', (error) => {
23 console.error('WebSocket error:', error);
24 });
25
26 // Send welcome message
27 ws.send(JSON.stringify({ type: 'welcome', message: 'Connected!' }));
28});
29
30server.listen(8080, () => {
31 console.log('WebSocket server running on port 8080');
32});Client (Browser)#
1class WebSocketClient {
2 private ws: WebSocket | null = null;
3 private reconnectAttempts = 0;
4 private maxReconnectAttempts = 5;
5 private handlers = new Map<string, Set<(data: any) => void>>();
6
7 connect(url: string): void {
8 this.ws = new WebSocket(url);
9
10 this.ws.onopen = () => {
11 console.log('Connected');
12 this.reconnectAttempts = 0;
13 };
14
15 this.ws.onmessage = (event) => {
16 const message = JSON.parse(event.data);
17 this.emit(message.type, message.data);
18 };
19
20 this.ws.onclose = () => {
21 console.log('Disconnected');
22 this.attemptReconnect(url);
23 };
24
25 this.ws.onerror = (error) => {
26 console.error('WebSocket error:', error);
27 };
28 }
29
30 private attemptReconnect(url: string): void {
31 if (this.reconnectAttempts >= this.maxReconnectAttempts) {
32 console.error('Max reconnection attempts reached');
33 return;
34 }
35
36 this.reconnectAttempts++;
37 const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
38
39 console.log(`Reconnecting in ${delay}ms...`);
40 setTimeout(() => this.connect(url), delay);
41 }
42
43 send(type: string, data: any): void {
44 if (this.ws?.readyState === WebSocket.OPEN) {
45 this.ws.send(JSON.stringify({ type, data }));
46 }
47 }
48
49 on(type: string, handler: (data: any) => void): void {
50 if (!this.handlers.has(type)) {
51 this.handlers.set(type, new Set());
52 }
53 this.handlers.get(type)!.add(handler);
54 }
55
56 private emit(type: string, data: any): void {
57 this.handlers.get(type)?.forEach(handler => handler(data));
58 }
59
60 close(): void {
61 this.ws?.close();
62 }
63}
64
65// Usage
66const client = new WebSocketClient();
67client.connect('wss://api.example.com/ws');
68
69client.on('notification', (data) => {
70 console.log('New notification:', data);
71});
72
73client.send('subscribe', { channel: 'updates' });Socket.IO#
Server#
1import { Server } from 'socket.io';
2import { createServer } from 'http';
3
4const httpServer = createServer();
5const io = new Server(httpServer, {
6 cors: {
7 origin: 'https://example.com',
8 methods: ['GET', 'POST'],
9 },
10});
11
12io.on('connection', (socket) => {
13 console.log('User connected:', socket.id);
14
15 // Join rooms
16 socket.on('join-room', (roomId: string) => {
17 socket.join(roomId);
18 socket.to(roomId).emit('user-joined', { userId: socket.id });
19 });
20
21 // Handle messages
22 socket.on('message', (data) => {
23 io.to(data.roomId).emit('message', {
24 ...data,
25 senderId: socket.id,
26 timestamp: new Date(),
27 });
28 });
29
30 // Handle disconnect
31 socket.on('disconnect', () => {
32 console.log('User disconnected:', socket.id);
33 });
34});
35
36httpServer.listen(3000);Client#
1import { io, Socket } from 'socket.io-client';
2
3const socket: Socket = io('https://api.example.com', {
4 auth: {
5 token: 'user-auth-token',
6 },
7 reconnection: true,
8 reconnectionAttempts: 5,
9 reconnectionDelay: 1000,
10});
11
12socket.on('connect', () => {
13 console.log('Connected:', socket.id);
14 socket.emit('join-room', 'room-123');
15});
16
17socket.on('message', (data) => {
18 console.log('New message:', data);
19});
20
21socket.on('disconnect', (reason) => {
22 console.log('Disconnected:', reason);
23});
24
25// Send message
26socket.emit('message', {
27 roomId: 'room-123',
28 content: 'Hello, world!',
29});Authentication#
Token-Based Auth#
1import jwt from 'jsonwebtoken';
2
3const wss = new WebSocketServer({ server });
4
5wss.on('connection', (ws, req) => {
6 // Get token from query string or header
7 const url = new URL(req.url!, `http://${req.headers.host}`);
8 const token = url.searchParams.get('token');
9
10 if (!token) {
11 ws.close(4001, 'Authentication required');
12 return;
13 }
14
15 try {
16 const user = jwt.verify(token, process.env.JWT_SECRET!);
17 (ws as any).user = user;
18 } catch (error) {
19 ws.close(4002, 'Invalid token');
20 return;
21 }
22
23 // Proceed with authenticated connection
24 handleConnection(ws);
25});Socket.IO Authentication#
1io.use((socket, next) => {
2 const token = socket.handshake.auth.token;
3
4 if (!token) {
5 return next(new Error('Authentication required'));
6 }
7
8 try {
9 const user = jwt.verify(token, process.env.JWT_SECRET!);
10 socket.data.user = user;
11 next();
12 } catch (error) {
13 next(new Error('Invalid token'));
14 }
15});Scaling WebSockets#
Redis Pub/Sub for Multiple Servers#
1import { createClient } from 'redis';
2
3const pubClient = createClient({ url: process.env.REDIS_URL });
4const subClient = pubClient.duplicate();
5
6await Promise.all([pubClient.connect(), subClient.connect()]);
7
8// Subscribe to channel
9await subClient.subscribe('notifications', (message) => {
10 const data = JSON.parse(message);
11
12 // Broadcast to local clients
13 wss.clients.forEach((client) => {
14 if (client.readyState === WebSocket.OPEN) {
15 client.send(JSON.stringify(data));
16 }
17 });
18});
19
20// Publish from any server
21async function broadcast(data: any): Promise<void> {
22 await pubClient.publish('notifications', JSON.stringify(data));
23}Socket.IO with Redis Adapter#
1import { createAdapter } from '@socket.io/redis-adapter';
2
3const pubClient = createClient({ url: process.env.REDIS_URL });
4const subClient = pubClient.duplicate();
5
6await Promise.all([pubClient.connect(), subClient.connect()]);
7
8io.adapter(createAdapter(pubClient, subClient));
9
10// Now io.emit() works across all servers
11io.emit('announcement', { message: 'Server maintenance in 5 minutes' });Connection Management#
Heartbeat/Ping-Pong#
1const HEARTBEAT_INTERVAL = 30000;
2const HEARTBEAT_TIMEOUT = 10000;
3
4wss.on('connection', (ws) => {
5 let isAlive = true;
6 let heartbeatTimeout: NodeJS.Timeout;
7
8 const heartbeat = setInterval(() => {
9 if (!isAlive) {
10 clearInterval(heartbeat);
11 ws.terminate();
12 return;
13 }
14
15 isAlive = false;
16 ws.ping();
17
18 heartbeatTimeout = setTimeout(() => {
19 ws.terminate();
20 }, HEARTBEAT_TIMEOUT);
21 }, HEARTBEAT_INTERVAL);
22
23 ws.on('pong', () => {
24 isAlive = true;
25 clearTimeout(heartbeatTimeout);
26 });
27
28 ws.on('close', () => {
29 clearInterval(heartbeat);
30 clearTimeout(heartbeatTimeout);
31 });
32});Connection Limits#
1const MAX_CONNECTIONS_PER_USER = 5;
2const connectionCounts = new Map<string, number>();
3
4wss.on('connection', (ws) => {
5 const userId = (ws as any).user.id;
6 const currentCount = connectionCounts.get(userId) || 0;
7
8 if (currentCount >= MAX_CONNECTIONS_PER_USER) {
9 ws.close(4003, 'Too many connections');
10 return;
11 }
12
13 connectionCounts.set(userId, currentCount + 1);
14
15 ws.on('close', () => {
16 const count = connectionCounts.get(userId) || 1;
17 connectionCounts.set(userId, count - 1);
18 });
19});Message Patterns#
Request-Response#
1// Server
2ws.on('message', async (data) => {
3 const { id, type, payload } = JSON.parse(data.toString());
4
5 try {
6 const result = await handleRequest(type, payload);
7 ws.send(JSON.stringify({ id, success: true, data: result }));
8 } catch (error) {
9 ws.send(JSON.stringify({ id, success: false, error: error.message }));
10 }
11});
12
13// Client
14class RequestResponseClient {
15 private pending = new Map<string, { resolve: Function; reject: Function }>();
16
17 constructor(private ws: WebSocket) {
18 ws.onmessage = (event) => {
19 const { id, success, data, error } = JSON.parse(event.data);
20 const handlers = this.pending.get(id);
21
22 if (handlers) {
23 if (success) {
24 handlers.resolve(data);
25 } else {
26 handlers.reject(new Error(error));
27 }
28 this.pending.delete(id);
29 }
30 };
31 }
32
33 async request<T>(type: string, payload: any): Promise<T> {
34 const id = crypto.randomUUID();
35
36 return new Promise((resolve, reject) => {
37 this.pending.set(id, { resolve, reject });
38 this.ws.send(JSON.stringify({ id, type, payload }));
39
40 // Timeout
41 setTimeout(() => {
42 if (this.pending.has(id)) {
43 this.pending.delete(id);
44 reject(new Error('Request timeout'));
45 }
46 }, 30000);
47 });
48 }
49}Error Handling#
1ws.on('error', (error) => {
2 console.error('WebSocket error:', error);
3
4 // Log but don't crash
5 if (error.code === 'ECONNRESET') {
6 console.log('Client disconnected abruptly');
7 }
8});
9
10// Graceful shutdown
11process.on('SIGTERM', () => {
12 wss.clients.forEach((client) => {
13 client.close(1001, 'Server shutting down');
14 });
15
16 wss.close(() => {
17 console.log('WebSocket server closed');
18 process.exit(0);
19 });
20});Conclusion#
WebSockets enable powerful real-time features, but require careful handling of connections, scaling, and errors. Use Socket.IO for easier development, or raw WebSockets for more control. Always implement reconnection logic and heartbeats for production reliability.
Remember: real-time features are complex. Start simple, test thoroughly, and scale gradually.