
The Power of Real-Time Communication
The Real-Time Revolution
- 70% of users expect real-time features in modern applications
- Real-time apps see 25% higher user engagement
- Live collaboration tools grew 300% during 2020-2024
- Gaming, finance, and collaboration tools drive 60% of real-time traffic
- Chat and messaging
- Live document collaboration (Google Docs style)
- Real-time analytics dashboards
- Multiplayer games
- Live streaming and video calls
- IoT device control
- Live auctions and trading platforms
Real-Time Technology Comparison
Technology Overview
Socket.io vs Raw WebSockets
Javascript
// Automatic reconnection
io.on('connection', (socket) => {
// Socket.io handles reconnection automatically
socket.on('disconnect', (reason) => {
if (reason === 'io server disconnect') {
// Server initiated, reconnect manually
socket.connect();
}
// Client disconnection - auto-reconnects
});
});
// Room management built-in
socket.join('room1');
io.to('room1').emit('message', 'Hello Room 1');
// Automatic fallback to polling
const io = new Server(httpServer, {
transports: ['websocket', 'polling'] // Automatic fallback
});
Javascript
// Manual reconnection logic
class WebSocketClient {
constructor(url) {
this.url = url;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectDelay = 1000; // Reset delay
};
this.ws.onclose = () => {
console.log('Disconnected, reconnecting...');
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
};
}
}
// No built-in room management - must implement on server
Server-Sent Events (SSE) for One-Way Updates
- Server → Client updates only
- Simpler than WebSockets
- Better mobile battery life
- Automatic reconnection
Javascript
// Server (Express)
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Send updates
const sendUpdate = () => {
const data = { time: new Date().toISOString() };
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
const interval = setInterval(sendUpdate, 1000);
req.on('close', () => {
clearInterval(interval);
});
});
// Client
const eventSource = new EventSource('/events');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Update:', data);
};
eventSource.onerror = (error) => {
console.error('SSE error:', error);
eventSource.close();
};
- No client → server communication (must use separate HTTP requests)
- Maximum 6 concurrent connections per domain
- Text-only (no binary data)
WebRTC for Peer-to-Peer
- Video/audio calls
- Direct file sharing
- Low-latency gaming
Javascript
// Simple WebRTC Data Channel
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// Create data channel
const dataChannel = peerConnection.createDataChannel('chat');
dataChannel.onopen = () => {
dataChannel.send('Hello peer!');
};
dataChannel.onmessage = (event) => {
console.log('Received:', event.data);
};
// WebRTC requires signaling server (can use Socket.io!)
io.on('connection', (socket) => {
socket.on('webrtc-offer', (offer) => {
socket.broadcast.emit('webrtc-offer', offer);
});
socket.on('webrtc-answer', (answer) => {
socket.broadcast.emit('webrtc-answer', answer);
});
});
Understanding WebSockets vs Socket.io
WebSockets: The Foundation
- Automatic reconnection
- Room management
- Broadcasting
- Fallback options
Socket.io: The Complete Solution
- Automatic fallback to HTTP long-polling
- Auto-reconnection with exponential backoff
- Room and namespace support
- Binary streaming capabilities
- Middleware support
- Built-in acknowledgements
Setting Up Your Socket.io Server
Javascript
// server.js
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const redis = require('redis');
const { createAdapter } = require('@socket.io/redis-adapter');
const app = express();
const httpServer = createServer(app);
// Configure Socket.io with Redis adapter for scaling
const pubClient = redis.createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
const io = new Server(httpServer, {
cors: {
origin: process.env.CLIENT_URL,
credentials: true
},
adapter: createAdapter(pubClient, subClient),
// Connection options
pingTimeout: 60000,
pingInterval: 25000,
upgradeTimeout: 10000,
maxHttpBufferSize: 1e6, // 1MB
transports: ['websocket', 'polling']
});
// Middleware for authentication
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
try {
const user = await verifyToken(token);
socket.userId = user.id;
socket.username = user.username;
next();
} catch (err) {
next(new Error('Authentication failed'));
}
});
// Middleware for rate limiting
const rateLimiter = new Map();
io.use((socket, next) => {
const userId = socket.userId;
const now = Date.now();
const userLimit = rateLimiter.get(userId) || { count: 0, resetTime: now + 60000 };
if (now > userLimit.resetTime) {
userLimit.count = 0;
userLimit.resetTime = now + 60000;
}
if (userLimit.count >= 100) {
return next(new Error('Rate limit exceeded'));
}
userLimit.count++;
rateLimiter.set(userId, userLimit);
next();
});
});
Building a Real-Time Chat Application
Server-Side Implementation
Javascript
// Chat room management
const rooms = new Map();
const activeUsers = new Map();
io.on('connection', (socket) => {
console.log(`User ${socket.userId} connected`);
// Track active user
activeUsers.set(socket.userId, {
socketId: socket.id,
username: socket.username,
connectedAt: Date.now()
});
// Broadcast user joined
socket.broadcast.emit('user-connected', {
userId: socket.userId,
username: socket.username
});
// Join a room
socket.on('join-room', async (roomId, callback) => {
try {
socket.join(roomId);
// Load message history
const messages = await getMessageHistory(roomId, 50);
// Get room members
const members = await io.in(roomId).allSockets();
const memberList = Array.from(members).map(socketId => {
const user = Array.from(activeUsers.values())
.find(u => u.socketId === socketId);
return user ? { userId: user.userId, username: user.username } : null;
}).filter(Boolean);
// Send initial data
callback({
success: true,
messages,
members: memberList
});
// Notify others in the room
socket.to(roomId).emit('user-joined', {
userId: socket.userId,
username: socket.username,
timestamp: Date.now()
});
console.log(`User ${socket.userId} joined room ${roomId}`);
} catch (error) {
callback({ success: false, error: error.message });
}
});
// Handle message sending with acknowledgement
socket.on('send-message', async (data, callback) => {
const { roomId, message } = data;
try {
// Validate message
if (!message || message.trim().length === 0) {
return callback({ success: false, error: 'Empty message' });
}
if (message.length > 1000) {
return callback({ success: false, error: 'Message too long' });
}
// Save message to database
const savedMessage = await saveMessage({
roomId,
userId: socket.userId,
username: socket.username,
message: message.trim(),
timestamp: Date.now()
});
// Broadcast to room members
io.to(roomId).emit('new-message', savedMessage);
// Acknowledge to sender
callback({ success: true, messageId: savedMessage.id });
} catch (error) {
callback({ success: false, error: error.message });
}
});
// Handle typing indicators with throttling
let typingTimeout;
socket.on('typing', ({ roomId, isTyping }) => {
clearTimeout(typingTimeout);
socket.to(roomId).emit('user-typing', {
userId: socket.userId,
username: socket.username,
isTyping
});
// Auto-stop typing after 3 seconds
if (isTyping) {
typingTimeout = setTimeout(() => {
socket.to(roomId).emit('user-typing', {
userId: socket.userId,
username: socket.username,
isTyping: false
});
}, 3000);
}
});
// Handle read receipts
socket.on('message-read', async ({ roomId, messageId }) => {
await markMessageAsRead(messageId, socket.userId);
socket.to(roomId).emit('message-read', {
messageId,
userId: socket.userId,
timestamp: Date.now()
});
});
// Leave room
socket.on('leave-room', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('user-left', {
userId: socket.userId,
username: socket.username,
timestamp: Date.now()
});
});
// Handle disconnection
socket.on('disconnect', (reason) => {
console.log(`User ${socket.userId} disconnected: ${reason}`);
// Remove from active users
activeUsers.delete(socket.userId);
// Notify all rooms
socket.broadcast.emit('user-disconnected', {
userId: socket.userId,
username: socket.username,
reason
});
});
// Error handling
socket.on('error', (error) => {
console.error(`Socket error for user ${socket.userId}:`, error);
});
});
Client-Side React Implementation
Jsx
import { useEffect, useState, useCallback, useRef } from 'react';
import io from 'socket.io-client';
function ChatRoom({ roomId, authToken }) {
const [socket, setSocket] = useState(null);
const [messages, setMessages] = useState([]);
const [members, setMembers] = useState([]);
const [typingUsers, setTypingUsers] = useState(new Set());
const [connectionStatus, setConnectionStatus] = useState('disconnected');
const reconnectAttempts = useRef(0);
useEffect(() => {
// Initialize socket
const newSocket = io(process.env.NEXT_PUBLIC_SERVER_URL, {
auth: {
token: authToken
},
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: 5
});
// Connection events
newSocket.on('connect', () => {
console.log('Connected to server');
setConnectionStatus('connected');
reconnectAttempts.current = 0;
// Join room with acknowledgement
newSocket.emit('join-room', roomId, (response) => {
if (response.success) {
setMessages(response.messages);
setMembers(response.members);
} else {
console.error('Failed to join room:', response.error);
}
});
});
newSocket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
setConnectionStatus('disconnected');
});
newSocket.on('connect_error', (error) => {
console.error('Connection error:', error);
setConnectionStatus('error');
reconnectAttempts.current++;
});
newSocket.on('reconnect', (attemptNumber) => {
console.log('Reconnected after', attemptNumber, 'attempts');
setConnectionStatus('connected');
});
newSocket.on('reconnect_attempt', () => {
setConnectionStatus('reconnecting');
});
// Message events
newSocket.on('new-message', (message) => {
setMessages(prev => [...prev, message]);
// Play notification sound if not sender
if (message.userId !== newSocket.id) {
playNotificationSound();
}
// Auto-scroll to bottom
scrollToBottom();
});
// Typing indicators
newSocket.on('user-typing', ({ userId, username, isTyping }) => {
setTypingUsers(prev => {
const updated = new Set(prev);
if (isTyping) {
updated.add(username);
} else {
updated.delete(username);
}
return updated;
});
});
// User presence
newSocket.on('user-joined', ({ userId, username }) => {
setMembers(prev => [...prev, { userId, username }]);
showToast(`${username} joined the room`);
});
newSocket.on('user-left', ({ userId, username }) => {
setMembers(prev => prev.filter(m => m.userId !== userId));
showToast(`${username} left the room`);
});
setSocket(newSocket);
return () => {
newSocket.emit('leave-room', roomId);
newSocket.close();
};
}, [roomId, authToken]);
const sendMessage = useCallback((message, callback) => {
if (!socket) return;
socket.emit('send-message', { roomId, message }, (response) => {
if (response.success) {
callback?.(null, response.messageId);
} else {
callback?.(response.error);
}
});
}, [socket, roomId]);
const sendTypingIndicator = useCallback((isTyping) => {
socket?.emit('typing', { roomId, isTyping });
}, [socket, roomId]);
return (
<div className="chat-room">
<ConnectionStatus status={connectionStatus} />
<MemberList members={members} />
<MessageList messages={messages} />
{typingUsers.size > 0 && (
<TypingIndicator users={Array.from(typingUsers)} />
)}
<MessageInput
onSend={sendMessage}
onTyping={sendTypingIndicator}
/>
</div>
);
}
Error Handling and Recovery Strategies
Automatic Reconnection with Exponential Backoff
Javascript
// Client-side reconnection logic
class RobustSocketConnection {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnectionDelay: 1000,
reconnectionDelayMax: 30000,
reconnectionAttempts: Infinity,
...options
};
this.reconnectAttempt = 0;
this.messageQueue = [];
this.connect();
}
connect() {
this.socket = io(this.url, {
...this.options,
reconnection: false // Handle manually
});
this.socket.on('connect', () => {
console.log('Connected');
this.reconnectAttempt = 0;
this.flushMessageQueue();
});
this.socket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
if (reason === 'io server disconnect') {
// Server initiated, try to reconnect
this.reconnect();
}
// Client disconnect - handle gracefully
});
this.socket.on('connect_error', (error) => {
console.error('Connection error:', error);
this.reconnect();
});
}
reconnect() {
if (this.reconnectAttempt >= this.options.reconnectionAttempts) {
console.error('Max reconnection attempts reached');
this.emit('max_reconnect_failed');
return;
}
this.reconnectAttempt++;
const delay = Math.min(
this.options.reconnectionDelay * Math.pow(2, this.reconnectAttempt - 1),
this.options.reconnectionDelayMax
);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
setTimeout(() => {
this.connect();
}, delay);
}
emit(event, data) {
if (this.socket.connected) {
this.socket.emit(event, data);
} else {
// Queue messages while disconnected
this.messageQueue.push({ event, data });
}
}
flushMessageQueue() {
while (this.messageQueue.length > 0) {
const { event, data } = this.messageQueue.shift();
this.socket.emit(event, data);
}
}
}
Message Queue During Disconnection
Javascript
// Queue messages and retry on reconnection
class MessageQueue {
constructor(socket) {
this.socket = socket;
this.queue = [];
this.maxQueueSize = 100;
this.retryInterval = 5000;
this.socket.on('connect', () => {
this.processQueue();
});
}
send(event, data, options = {}) {
const message = {
id: generateId(),
event,
data,
timestamp: Date.now(),
attempts: 0,
maxAttempts: options.maxAttempts || 3,
callback: options.callback
};
if (this.socket.connected) {
this.sendMessage(message);
} else {
this.enqueue(message);
}
}
enqueue(message) {
if (this.queue.length >= this.maxQueueSize) {
// Remove oldest message
const removed = this.queue.shift();
removed.callback?.(new Error('Message queue overflow'));
}
this.queue.push(message);
this.saveToLocalStorage();
}
sendMessage(message) {
this.socket.emit(message.event, message.data, (response) => {
if (response?.success) {
message.callback?.(null, response);
} else {
message.attempts++;
if (message.attempts < message.maxAttempts) {
// Retry after delay
setTimeout(() => this.sendMessage(message), this.retryInterval);
} else {
message.callback?.(new Error('Max retries exceeded'));
}
}
});
}
processQueue() {
while (this.queue.length > 0 && this.socket.connected) {
const message = this.queue.shift();
this.sendMessage(message);
}
this.saveToLocalStorage();
}
saveToLocalStorage() {
try {
localStorage.setItem('messageQueue', JSON.stringify(this.queue));
} catch (error) {
console.error('Failed to save queue:', error);
}
}
loadFromLocalStorage() {
try {
const saved = localStorage.getItem('messageQueue');
if (saved) {
this.queue = JSON.parse(saved);
}
} catch (error) {
console.error('Failed to load queue:', error);
}
}
}
Conflict Resolution in Distributed Systems
Javascript
// Operational Transformation for collaborative editing
class OTEngine {
constructor() {
this.revision = 0;
this.buffer = [];
}
// Transform operation against concurrent operations
transform(op1, op2) {
if (op1.position < op2.position) {
return op1; // No change needed
}
if (op1.type === 'insert' && op2.type === 'insert') {
// Both insertions - adjust position
return {
...op1,
position: op1.position + op2.text.length
};
}
if (op1.type === 'delete' && op2.type === 'insert') {
// Delete after insert
return {
...op1,
position: op1.position + op2.text.length
};
}
// Additional transformation rules...
return op1;
}
applyOperation(operation) {
// Transform against pending operations
let transformedOp = operation;
for (const pendingOp of this.buffer) {
transformedOp = this.transform(transformedOp, pendingOp);
}
return transformedOp;
}
}
Scaling Socket.io Applications
Horizontal Scaling with Redis
Javascript
// Use Redis adapter for multi-server deployment
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
// Emit to all servers
io.emit('global-event', data);
// Emit to specific room across all servers
io.to('room-123').emit('room-event', data);
// Get all socket IDs in a room (across all servers)
const sockets = await io.in('room-123').allSockets();
console.log('Sockets in room:', sockets.size);
Load Balancing with Sticky Sessions
Nginx
# nginx.conf
upstream socketio {
ip_hash; # Enable sticky sessions
server backend1:3000;
server backend2:3000;
server backend3:3000;
}
server {
listen 80;
location /socket.io/ {
proxy_pass http://socketio;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
# Timeouts
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
}
Advanced Features
Binary Data Streaming
Javascript
// Server: Stream file chunks
socket.on('request-file', async (fileId) => {
const filePath = path.join(__dirname, 'files', fileId);
const fileSize = fs.statSync(filePath).size;
const stream = fs.createReadStream(filePath, {
highWaterMark: 64 * 1024 // 64KB chunks
});
let bytesRead = 0;
stream.on('data', (chunk) => {
bytesRead += chunk.length;
socket.emit('file-chunk', {
fileId,
chunk: chunk, // Binary data
progress: bytesRead / fileSize,
bytesRead,
fileSize
});
});
stream.on('end', () => {
socket.emit('file-complete', { fileId });
});
stream.on('error', (error) => {
socket.emit('file-error', { fileId, error: error.message });
});
});
// Client: Receive and reconstruct file
const chunks = [];
let receivedBytes = 0;
socket.on('file-chunk', ({ fileId, chunk, progress, bytesRead, fileSize }) => {
chunks.push(chunk);
receivedBytes = bytesRead;
updateProgressBar(progress);
console.log(`Received ${receivedBytes} / ${fileSize} bytes`);
});
socket.on('file-complete', ({ fileId }) => {
const blob = new Blob(chunks);
const url = URL.createObjectURL(blob);
// Trigger download
const a = document.createElement('a');
a.href = url;
a.download = fileId;
a.click();
// Cleanup
chunks.length = 0;
URL.revokeObjectURL(url);
});
Real-Time Analytics Dashboard
Javascript
// Efficient metrics broadcasting
class MetricsAggregator {
constructor(io) {
this.io = io;
this.metrics = new Map();
this.updateInterval = 1000; // 1 second
this.startAggregation();
}
startAggregation() {
setInterval(async () => {
const metrics = await this.collectMetrics();
// Broadcast to analytics room
this.io.to('analytics').emit('metrics-update', metrics);
}, this.updateInterval);
}
async collectMetrics() {
return {
activeUsers: this.io.sockets.sockets.size,
roomCounts: await this.getRoomCounts(),
messageRate: this.calculateMessageRate(),
serverLoad: await this.getServerLoad(),
timestamp: Date.now()
};
}
async getRoomCounts() {
const rooms = this.io.sockets.adapter.rooms;
const counts = {};
for (const [roomId, socketIds] of rooms) {
if (!socketIds.has(roomId)) { // Exclude individual socket rooms
counts[roomId] = socketIds.size;
}
}
return counts;
}
}
// Client: Efficient real-time charting
function AnalyticsDashboard() {
const [metrics, setMetrics] = useState([]);
const maxDataPoints = 60; // Last 60 seconds
useEffect(() => {
const socket = io('/analytics');
socket.emit('join-room', 'analytics');
socket.on('metrics-update', (newMetrics) => {
setMetrics(prev => {
const updated = [...prev, newMetrics];
// Keep only recent data
return updated.slice(-maxDataPoints);
});
});
return () => socket.disconnect();
}, []);
return <RealtimeChart data={metrics} />;
}
Presence Tracking System
Javascript
// Server: Track user presence
class PresenceTracker {
constructor(io) {
this.io = io;
this.presence = new Map();
io.on('connection', (socket) => {
this.handleConnection(socket);
});
}
handleConnection(socket) {
const userId = socket.userId;
// Add user to presence
this.presence.set(userId, {
socketId: socket.id,
status: 'online',
lastSeen: Date.now(),
metadata: socket.handshake.query
});
// Broadcast presence update
socket.broadcast.emit('presence-update', {
userId,
status: 'online'
});
// Handle status changes
socket.on('status-change', (status) => {
const user = this.presence.get(userId);
if (user) {
user.status = status;
user.lastSeen = Date.now();
this.io.emit('presence-update', {
userId,
status
});
}
});
// Handle disconnection
socket.on('disconnect', () => {
const user = this.presence.get(userId);
if (user) {
user.status = 'offline';
user.lastSeen = Date.now();
// Keep presence data for 30 days
setTimeout(() => {
this.presence.delete(userId);
}, 30 * 24 * 60 * 60 * 1000);
socket.broadcast.emit('presence-update', {
userId,
status: 'offline',
lastSeen: user.lastSeen
});
}
});
}
getPresence(userId) {
return this.presence.get(userId);
}
getOnlineUsers() {
return Array.from(this.presence.values())
.filter(user => user.status === 'online');
}
}
Real-Time Notifications System
Javascript
// Server: Notification management
class NotificationManager {
constructor(io) {
this.io = io;
}
async sendNotification(userId, notification) {
// Find user's socket
const userSockets = await this.findUserSockets(userId);
if (userSockets.length > 0) {
// User is online - send real-time notification
userSockets.forEach(socketId => {
this.io.to(socketId).emit('notification', {
id: generateId(),
...notification,
timestamp: Date.now(),
read: false
});
});
} else {
// User is offline - store for later retrieval
await this.storeNotification(userId, notification);
// Send push notification
await this.sendPushNotification(userId, notification);
}
}
async findUserSockets(userId) {
const sockets = await this.io.fetchSockets();
return sockets
.filter(socket => socket.userId === userId)
.map(socket => socket.id);
}
async sendPushNotification(userId, notification) {
// Integrate with push notification service (FCM, APNS, etc.)
const pushTokens = await getPushTokens(userId);
for (const token of pushTokens) {
await sendPush(token, {
title: notification.title,
body: notification.body,
data: notification.data
});
}
}
}
// Client: Notification handling
function useNotifications() {
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => {
const socket = io();
// Load unread notifications on connect
socket.on('connect', async () => {
const unread = await fetch('/api/notifications/unread').then(r => r.json());
setNotifications(unread);
setUnreadCount(unread.length);
});
// Handle new notifications
socket.on('notification', (notification) => {
setNotifications(prev => [notification, ...prev]);
setUnreadCount(prev => prev + 1);
// Show browser notification
if (Notification.permission === 'granted') {
new Notification(notification.title, {
body: notification.body,
icon: '/icon.png'
});
}
// Play sound
playNotificationSound();
});
return () => socket.disconnect();
}, []);
const markAsRead = async (notificationId) => {
await fetch(`/api/notifications/${notificationId}/read`, { method: 'POST' });
setNotifications(prev =>
prev.map(n => n.id === notificationId ? { ...n, read: true } : n)
);
setUnreadCount(prev => Math.max(0, prev - 1));
};
return { notifications, unreadCount, markAsRead };
}
Testing Real-Time Features
Unit Testing Socket.io Handlers
Javascript
// __tests__/socket.test.js
const { createServer } = require('http');
const { Server } = require('socket.io');
const Client = require('socket.io-client');
describe('Socket.io Chat', () => {
let io, serverSocket, clientSocket;
beforeAll((done) => {
const httpServer = createServer();
io = new Server(httpServer);
httpServer.listen(() => {
const port = httpServer.address().port;
clientSocket = Client(`http://localhost:${port}`);
io.on('connection', (socket) => {
serverSocket = socket;
});
clientSocket.on('connect', done);
});
});
afterAll(() => {
io.close();
clientSocket.close();
});
test('should send and receive messages', (done) => {
const message = 'Hello World';
clientSocket.on('new-message', (msg) => {
expect(msg).toBe(message);
done();
});
serverSocket.emit('new-message', message);
});
test('should join room', (done) => {
const roomId = 'room-123';
serverSocket.on('join-room', (room, callback) => {
expect(room).toBe(roomId);
callback({ success: true });
done();
});
clientSocket.emit('join-room', roomId, (response) => {
expect(response.success).toBe(true);
});
});
});
Integration Testing with Multiple Clients
Javascript
describe('Multi-client chat', () => {
let io, client1, client2;
beforeEach((done) => {
const httpServer = createServer();
io = new Server(httpServer);
httpServer.listen(() => {
const port = httpServer.address().port;
client1 = Client(`http://localhost:${port}`);
client2 = Client(`http://localhost:${port}`);
let connected = 0;
const checkDone = () => {
connected++;
if (connected === 2) done();
};
client1.on('connect', checkDone);
client2.on('connect', checkDone);
});
});
test('messages broadcast to other clients', (done) => {
const message = 'Broadcast test';
const roomId = 'test-room';
// Both clients join room
client1.emit('join-room', roomId);
client2.emit('join-room', roomId);
// Client 2 listens for message
client2.on('new-message', (msg) => {
expect(msg.content).toBe(message);
done();
});
// Client 1 sends message
setTimeout(() => {
client1.emit('send-message', { roomId, message });
}, 100);
});
});
Load Testing with Artillery
Yaml
# artillery-load-test.yml
config:
target: "http://localhost:3000"
phases:
- duration: 60
arrivalRate: 10 # 10 new connections per second
name: "Warm up"
- duration: 120
arrivalRate: 50 # Ramp up to 50/sec
name: "Sustained load"
- duration: 60
arrivalRate: 100 # Peak load
name: "Peak load"
engines:
socketio: {}
scenarios:
- name: "Chat simulation"
engine: socketio
flow:
- emit:
channel: "join-room"
data:
roomId: "load-test-room"
- think: 2
- emit:
channel: "send-message"
data:
roomId: "load-test-room"
message: "Load test message"
- think: 5
- loop:
- emit:
channel: "send-message"
data:
message: "{{ $randomString() }}"
- think: 3
count: 10
Bash
# Run load test
artillery run artillery-load-test.yml --output report.json
# Generate report
artillery report report.json
Performance Optimization
Connection Management
Javascript
// Implement heartbeat for connection health
io.on('connection', (socket) => {
let isAlive = true;
socket.on('pong', () => {
isAlive = true;
});
const interval = setInterval(() => {
if (!isAlive) {
console.log('Socket unresponsive, terminating');
socket.disconnect();
return clearInterval(interval);
}
isAlive = false;
socket.emit('ping');
}, 30000); // Check every 30 seconds
socket.on('disconnect', () => {
clearInterval(interval);
});
});
Message Compression
Javascript
const io = new Server(httpServer, {
perMessageDeflate: {
threshold: 1024, // Compress messages larger than 1KB
zlibDeflateOptions: {
chunkSize: 1024,
memLevel: 7,
level: 3 // 0-9 compression level
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
clientNoContextTakeover: true,
serverNoContextTakeover: true,
serverMaxWindowBits: 10,
concurrencyLimit: 10
}
});
Event Throttling
Javascript
// Throttle high-frequency events
import throttle from 'lodash/throttle';
const throttledEmit = throttle((data) => {
io.emit('high-frequency-data', data);
}, 100); // Max 10 updates per second
// Debounce typing indicators
import debounce from 'lodash/debounce';
const debouncedTyping = debounce((socket, roomId) => {
socket.to(roomId).emit('user-typing', {
userId: socket.userId,
isTyping: false
});
}, 3000);
Production Deployment
SSL/TLS Configuration
Javascript
const fs = require('fs');
const https = require('https');
const server = https.createServer({
key: fs.readFileSync('/path/to/private.key'),
cert: fs.readFileSync('/path/to/certificate.crt'),
ca: fs.readFileSync('/path/to/ca_bundle.crt')
}, app);
const io = new Server(server, {
cors: {
origin: process.env.ALLOWED_ORIGINS.split(','),
credentials: true
}
});
server.listen(443);
Monitoring and Health Checks
Javascript
// Health check endpoint
app.get('/health', (req, res) => {
const health = {
status: 'ok',
timestamp: Date.now(),
uptime: process.uptime(),
connections: io.sockets.sockets.size,
memory: process.memoryUsage(),
redis: pubClient.isReady ? 'connected' : 'disconnected'
};
res.json(health);
});
// Prometheus metrics
const promClient = require('prom-client');
const activeConnections = new promClient.Gauge({
name: 'socketio_active_connections',
help: 'Number of active Socket.io connections'
});
const messagesTotal = new promClient.Counter({
name: 'socketio_messages_total',
help: 'Total number of messages',
labelNames: ['event']
});
io.on('connection', (socket) => {
activeConnections.inc();
socket.onAny((event) => {
messagesTotal.labels(event).inc();
});
socket.on('disconnect', () => {
activeConnections.dec();
});
});
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
Graceful Shutdown
Javascript
process.on('SIGTERM', async () => {
console.log('SIGTERM signal received: closing HTTP server');
// Stop accepting new connections
httpServer.close(() => {
console.log('HTTP server closed');
});
// Notify all clients
io.emit('server-shutdown', {
message: 'Server is shutting down',
reconnect: true
});
// Wait for clients to disconnect
await new Promise(resolve => setTimeout(resolve, 5000));
// Close Socket.io
io.close(() => {
console.log('Socket.io closed');
});
// Close Redis connections
await pubClient.quit();
await subClient.quit();
process.exit(0);
});
Security Best Practices
- Always authenticate connections using JWT or session tokens
- Validate all incoming data to prevent injection attacks
Javascript
const Joi = require('joi');
const messageSchema = Joi.object({
roomId: Joi.string().uuid().required(),
message: Joi.string().max(1000).required()
});
socket.on('send-message', (data, callback) => {
const { error, value } = messageSchema.validate(data);
if (error) {
return callback({ success: false, error: 'Invalid data' });
}
// Process validated data
handleMessage(value);
});
- Implement rate limiting to prevent spam
Javascript
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: 'Too many requests'
});
io.use((socket, next) => {
limiter(socket.request, {}, next);
});
- Use namespaces to separate different features
Javascript
const chatNamespace = io.of('/chat');
const notificationsNamespace = io.of('/notifications');
const adminNamespace = io.of('/admin');
// Different authentication for admin
adminNamespace.use(async (socket, next) => {
const user = await verifyToken(socket.handshake.auth.token);
if (user.role !== 'admin') {
return next(new Error('Unauthorized'));
}
next();
});
- Enable CORS with specific origins only
- Encrypt sensitive data before transmission
Conclusion
- Choose the right technology: Socket.io for general real-time, WebRTC for P2P, SSE for one-way updates
- Handle errors gracefully: Implement reconnection, message queuing, and conflict resolution
- Test thoroughly: Unit tests, integration tests, and load tests
- Scale horizontally: Use Redis adapter and load balancing
- Monitor continuously: Track connections, message rates, and errors
- Secure by default: Authentication, validation, rate limiting