Asia/Dhaka
Posts

Building Real-Time Applications with Socket.io and Node.js

October 5, 2024
Node.js Socket.io Real-Time Chat Application
Real-time applications have become essential in modern web development. From instant messaging to live collaboration tools, users expect immediate feedback and seamless synchronization across devices. Industry adoption:
  • 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
Use cases evolving:
  • 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
Socket.io advantages:
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
});
Raw WebSockets require manual implementation:
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
When to use SSE:
  • 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();
};
Limitations:
  • No client → server communication (must use separate HTTP requests)
  • Maximum 6 concurrent connections per domain
  • Text-only (no binary data)
Use for:
  • 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);
  });
});
WebSockets provide a persistent, bidirectional communication channel between client and server. However, they lack built-in features for:
  • Automatic reconnection
  • Room management
  • Broadcasting
  • Fallback options
Socket.io builds upon WebSockets, adding:
  • Automatic fallback to HTTP long-polling
  • Auto-reconnection with exponential backoff
  • Room and namespace support
  • Binary streaming capabilities
  • Middleware support
  • Built-in acknowledgements
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();
  });
});
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);
  });
});
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>
  );
}
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);
    }
  }
}
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);
    }
  }
}
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;
  }
}
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);
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;
    }
}
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);
});
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} />;
}
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');
  }
}
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 };
}
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);
    });
  });
});
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);
  });
});
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
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);
  });
});
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
  }
});
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);
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);
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());
});
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);
});
  1. Always authenticate connections using JWT or session tokens
  2. 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);
});
  1. 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);
});
  1. 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();
});
  1. Enable CORS with specific origins only
  2. Encrypt sensitive data before transmission
Socket.io provides a robust foundation for building real-time applications. By combining it with proper scaling strategies, security measures, and performance optimizations, you can create responsive, interactive applications that delight users. Key takeaways:
  1. Choose the right technology: Socket.io for general real-time, WebRTC for P2P, SSE for one-way updates
  2. Handle errors gracefully: Implement reconnection, message queuing, and conflict resolution
  3. Test thoroughly: Unit tests, integration tests, and load tests
  4. Scale horizontally: Use Redis adapter and load balancing
  5. Monitor continuously: Track connections, message rates, and errors
  6. Secure by default: Authentication, validation, rate limiting
Whether you're building a chat application, collaborative editor, or real-time dashboard, Socket.io's flexibility and reliability make it an excellent choice for modern web applications. Start with the basics, add advanced features as needed, and always prioritize user experience and security.
On this page
Hey! Got questions?Ask me anything 👋