LatteStream®
Learn the essential best practices for implementing WebSockets in production, from connection management to scaling strategies.
LatteStream Team
October 22, 2025
6 min read
Building real-time applications with WebSockets can be challenging. After helping other companies implement WebSocket infrastructure, we've compiled the essential best practices that will help you build robust, scalable, and maintainable real-time applications.
Network interruptions are inevitable, and WebSocket connections expect perfect conditions. Your WebSocket client should automatically attempt to reconnect when a connection is lost:
class ResilientWebSocket { constructor(url) { this.url = url; this.reconnectInterval = 1000; this.maxReconnectInterval = 30000; this.reconnectDecay = 1.5; this.reconnectAttempts = 0; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log('Connected'); this.reconnectAttempts = 0; // Start up logic here }; this.ws.onclose = () => { // Add logic here if you have an intentional disconnect this.reconnect(); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; } reconnect() { this.reconnectAttempts++; const timeout = Math.min( this.reconnectInterval * Math.pow(this.reconnectDecay, this.reconnectAttempts), this.maxReconnectInterval ); setTimeout(() => { console.log('Reconnecting...'); this.connect(); }, timeout); } }
Keep connections alive and detect stale connections early. On low-bandwidth and spotty devices this is key to ensure WebSocket connection state is consistent:
class HeartbeatWebSocket { startHeartbeat() { this.pingInterval = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'ping' })); this.pongTimeout = setTimeout(() => { console.log('Connection timeout'); this.ws.close(); }, 5000); } }, 30000); } handleMessage(message) { const data = JSON.parse(message); if (data.type === 'pong') { clearTimeout(this.pongTimeout); } } }
Queue messages when the connection is unavailable or your sending messages on an interval:
class QueuedWebSocket { constructor() { this.messageQueue = []; this.isConnected = false; } send(message) { if (this.isConnected && this.ws.readyState === WebSocket.OPEN) { this.ws.send(message); } else { this.messageQueue.push(message); } } onConnect() { this.isConnected = true; // Flush message queue while (this.messageQueue.length > 0) { const message = this.messageQueue.shift(); this.ws.send(message); } } }
Ensure critical messages are delivered:
class AcknowledgedWebSocket { constructor() { this.pendingMessages = new Map(); this.messageId = 0; } sendWithAck(message, timeout = 5000) { return new Promise((resolve, reject) => { const id = ++this.messageId; const timeoutId = setTimeout(() => { this.pendingMessages.delete(id); reject(new Error('Message acknowledgment timeout')); }, timeout); this.pendingMessages.set(id, { resolve, timeoutId }); this.ws.send( JSON.stringify({ id, ...message, }) ); }); } handleAck(messageId) { const pending = this.pendingMessages.get(messageId); if (pending) { clearTimeout(pending.timeoutId); pending.resolve(); this.pendingMessages.delete(messageId); } } }
Never trust unauthenticated WebSocket connections:
// Server-side authentication wss.on('connection', async (ws, req) => { const token = req.headers.authorization; try { const user = await verifyToken(token); ws.userId = user.id; ws.authenticated = true; } catch (error) { ws.close(1008, 'Invalid authentication'); return; } ws.on('message', (message) => { if (!ws.authenticated) { ws.close(1008, 'Not authenticated'); return; } // Process authenticated message }); });
Always validate incoming messages. While validating messages can also be emitted by type to various parts of the application:
const messageSchema = { type: 'object', properties: { type: { type: 'string', enum: ['chat', 'status', 'command'] }, payload: { type: 'object' }, timestamp: { type: 'number' }, }, required: ['type', 'payload'], additionalProperties: false, }; function validateMessage(message) { try { const data = JSON.parse(message); if (!ajv.validate(messageSchema, data)) { throw new Error('Invalid message format'); } return data; } catch (error) { console.error('Message validation failed:', error); return null; } }
Limit the number of concurrent connections per client:
class ConnectionPool { constructor(maxConnections = 5) { this.connections = []; this.maxConnections = maxConnections; this.currentIndex = 0; } getConnection() { if (this.connections.length < this.maxConnections) { const ws = new WebSocket(this.url); this.connections.push(ws); return ws; } // Round-robin selection const connection = this.connections[this.currentIndex]; this.currentIndex = (this.currentIndex + 1) % this.connections.length; return connection; } }
Protect your servers from abuse. Use windows to calculate allowed usage for more natural limits for WebSockets which can be bursty:
class RateLimiter { constructor(maxMessages = 100, windowMs = 60000) { this.maxMessages = maxMessages; this.windowMs = windowMs; this.clients = new Map(); } isAllowed(clientId) { const now = Date.now(); const client = this.clients.get(clientId) || { count: 0, resetTime: now + this.windowMs, }; if (now > client.resetTime) { client.count = 0; client.resetTime = now + this.windowMs; } if (client.count >= this.maxMessages) { return false; } client.count++; this.clients.set(clientId, client); return true; } }
For high-frequency updates, consider using binary formats:
// Using msgpack for efficient serialization import msgpack from 'msgpack-lite'; function sendBinaryMessage(ws, data) { const packed = msgpack.encode(data); ws.send(packed); } ws.on('message', (data) => { if (data instanceof Buffer) { const unpacked = msgpack.decode(data); processMessage(unpacked); } else { // Handle text message processMessage(JSON.parse(data)); } });
Reduce overhead by batching multiple messages. This is essential for queuing messages from disparate parts of the system:
class BatchedWebSocket { constructor() { this.batchQueue = []; this.batchInterval = 100; // ms this.startBatching(); } startBatching() { setInterval(() => { if (this.batchQueue.length > 0) { this.ws.send( JSON.stringify({ type: 'batch', messages: this.batchQueue, }) ); this.batchQueue = []; } }, this.batchInterval); } sendBatched(message) { this.batchQueue.push(message); } }
Track connection lifecycle and errors:
class MonitoredWebSocket { constructor() { this.metrics = { connectionsOpened: 0, connectionsClosed: 0, messagesReceived: 0, messagesSent: 0, errors: 0, }; } logMetrics() { console.log('WebSocket Metrics:', { ...this.metrics, activeConnections: this.metrics.connectionsOpened - this.metrics.connectionsClosed, timestamp: new Date().toISOString(), }); } setupMonitoring(ws) { ws.on('open', () => { this.metrics.connectionsOpened++; console.log('Connection opened', { timestamp: Date.now() }); }); ws.on('close', (code, reason) => { this.metrics.connectionsClosed++; console.log('Connection closed', { code, reason, timestamp: Date.now() }); }); ws.on('error', (error) => { this.metrics.errors++; console.error('WebSocket error', { error, timestamp: Date.now() }); }); } }
Implementing WebSockets in production requires careful consideration of connection management, security, scaling, and monitoring. By following these best practices, you'll build more reliable and scalable real-time applications.
Remember, while these practices provide a solid foundation, tools like LatteStream handle all of these complexities for you, allowing you to focus on building features rather than infrastructure.
Ready to implement these best practices without the hassle? Try LatteStream and get enterprise-grade WebSocket infrastructure in minutes.
LatteStream
WebSocket hosting that saves you a latte (seriously, start free).
Product
© 2026 LatteStream, LLC. All rights reserved.
Made in California