Facebook Pixel

Socket.io Best Practices and Performance Optimizing Real-Time Node.js

Learn best practices for Socket.io in production performance optimization, memory management, connection limits, and monitoring for large-scale real-time apps.

Socket.io Best Practices and Performance

Optimizing Socket.io for production ensures your real-time app handles thousands of concurrent connections efficiently.

1. Use WebSocket Transport Only

const io = socketio(server, { transports: ["websocket"], // Skip HTTP long-polling fallback cors: { origin: process.env.CLIENT_URL, credentials: true } });

By default, Socket.io starts with HTTP polling and upgrades to WebSocket. In production, skip polling for faster connections.

2. Limit Connection Buffer

io.engine.setMaxListeners(10000); // Max connections

3. Clean Up on Disconnect

io.on("connection", (socket) => { onlineUsers.set(socket.userId, socket.id); socket.on("disconnect", () => { onlineUsers.delete(socket.userId); // Leave all rooms (automatic, but clean up other resources) clearTimeout(socket.activityTimeout); }); });

4. Use Socket.io with PM2 Cluster

// ecosystem.config.js module.exports = { apps: [{ name: "devtinder-api", script: "src/server.js", instances: "max", exec_mode: "cluster", env: { REDIS_URL: "redis://localhost:6379" } }] };

With the Redis adapter, Socket.io works across all cluster workers.

5. Limit Message Size

io.on("connection", (socket) => { socket.on("message:send", (data) => { if (JSON.stringify(data).length > 10000) { return socket.emit("error", { message: "Message too large" }); } // Process }); });

6. Use Compression

const io = socketio(server, { // ... other options ... httpCompression: true, perMessageDeflate: { threshold: 1024 // Only compress messages > 1KB } });

7. Monitor Connections

// Periodically log connection stats setInterval(() => { const connectedClients = io.engine.clientsCount; const rooms = io.sockets.adapter.rooms.size; console.log(`Connected: ${connectedClients}, Rooms: ${rooms}`); }, 60000);

8. Use the Redis Adapter for Scaling

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));

9. Handle Memory Leaks

// Monitor memory usage setInterval(() => { const used = process.memoryUsage(); console.log({ rss: `${Math.round(used.rss / 1024 / 1024)} MB`, heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`, heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB` }); }, 300000); // Every 5 minutes // Clean up stale data setInterval(() => { // Remove offline users from the online map for (const [userId, socketId] of onlineUsers) { if (!io.sockets.sockets.get(socketId)) { onlineUsers.delete(userId); } } }, 300000);

10. Use a Heartbeat for Custom Presence

io.on("connection", (socket) => { // Client sends heartbeat every 30 seconds socket.on("heartbeat", () => { socket.lastActivity = Date.now(); }); // Check for inactive sockets setInterval(() => { if (Date.now() - (socket.lastActivity || 0) > 90000) { socket.disconnect(true); // Force disconnect after 90s of inactivity } }, 30000); });

Best Practices Checklist

  • WebSocket transport only (skip polling)
  • Redis adapter for multi-instance scaling
  • PM2 cluster mode with Redis adapter
  • Clean up on disconnect (online users, timeouts)
  • Limit message size
  • Rate limit events per user
  • Monitor connection count and memory
  • Handle memory leaks (clean stale data)
  • Use acknowledgments for important events
  • Rejoin rooms on reconnection
  • Input validation on all events
  • JWT authentication on every connection
  • HTTPS/WSS in production

The Takeaway

Socket.io performance best practices include: using WebSocket transport only (skip polling), Redis adapter for multi-instance scaling, PM2 cluster mode, cleaning up on disconnect, limiting message size, rate limiting events, monitoring connections and memory, handling memory leaks with periodic cleanup, using heartbeats for custom presence, and using acknowledgments for important events. These practices ensure your real-time app scales to thousands of concurrent connections.

Use WebSocket transport only (transports: ['websocket']), Redis adapter for multi-instance scaling, PM2 cluster mode, limit message size, rate limit events, clean up on disconnect, monitor connections and memory, handle memory leaks, and use acknowledgments for important events.

Use io.engine.clientsCount for total connections, io.sockets.adapter.rooms.size for room count, and process.memoryUsage() for memory. Log periodically with setInterval. Use @socket.io/admin-ui for a visual dashboard. Set up CloudWatch or Datadog for production monitoring.

Periodically clean up stale data: iterate through the online users map and remove entries where the socket no longer exists (io.sockets.sockets.get(socketId) returns undefined). Monitor memory with process.memoryUsage() and restart PM2 workers if memory grows too high (max_memory_restart in ecosystem.config.js).

Yes, in production. Use transports: ['websocket'] to skip the initial HTTP polling and upgrade. This gives faster connections and less overhead. HTTP polling is only needed for very old browsers or restrictive proxies that don't support WebSockets.

Set exec_mode: 'cluster' and instances: 'max' in PM2 ecosystem file. Install the Redis adapter (@socket.io/redis-adapter) so messages broadcast across all cluster workers. Set REDIS_URL in the env config. Without the Redis adapter, messages only reach clients on the same worker.

Ready to master Node.js completely?

Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.

Please Login.
Please Login.
Please Login.
Please Login.