Back to Blog
WebSocketsReal-TimeArchitectureScaling

WebSockets and Real-Time Communication: A Complete Guide

Build real-time features with WebSockets. From connection handling to scaling to fallback strategies for production systems.

B
Bootspring Team
Engineering
March 20, 2025
6 min read

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.

Share this article

Help spread the word about Bootspring