Building Real-Time Applications with WebSockets and Node.js
Implement bidirectional real-time communication in Node.js. Covers WebSocket basics, Socket.io for fallback support, scaling with Redis adapter, and production considerations for 10k+ concurrent connections.

HTTP is half-duplex — the client requests, the server responds. For live chat, gaming, or financial tickers, you need full-duplex communication. WebSockets provide persistent, bidirectional channels. This guide covers WebSocket implementation from basic to production-scale with Node.js.
Native WebSockets vs Socket.io
Node.js has built-in ws support via the ws npm package (or native --experimental-websockets in Node.js 22+). Socket.io adds automatic fallback (long-polling), rooms, reconnection, and broadcast — essential for production.
Basic Socket.io Server with Express
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: { origin: process.env.CLIENT_URL },
transports: [‘websocket’, ‘polling’], // Allow polling fallback
});
io.on(‘connection’, (socket) => {
console.log(User connected: ${socket.id});
socket.on(‘join-room’, (roomId) => {
socket.join(roomId);
io.to(roomId).emit(‘user-joined’, { userId: socket.id });
});
socket.on(‘chat-message’, (data) => {
// Broadcast to everyone in the room except sender
socket.to(data.room).emit(‘message’, {
userId: socket.id,
text: data.text,
timestamp: Date.now()
});
});
socket.on(‘disconnect’, () => {
console.log(User disconnected: ${socket.id});
});
});
httpServer.listen(3000);
Scaling WebSockets with Redis Adapter
When you have multiple Node.js instances (e.g., via Cluster or horizontal scaling), WebSocket connections are server-affine. Use Redis pub/sub to broadcast across instances:
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from '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));
// Now `io.emit('event', data)` works across all server instances
With Redis adapter, Socket.io handles automatic routing — messages to a user connected to a different server are routed via Redis.
Real-time Dashboard: Stock Ticker Example
Simulating 1000 stock price updates per second:
// Producer: Generate stock updates
setInterval(() => {
const stock = {
symbol: ['AAPL', 'GOOGL', 'MSFT'][Math.floor(Math.random() * 3)],
price: 100 + Math.random() * 50,
timestamp: Date.now()
};
io.emit('stock-update', stock); // Broadcast to all clients
}, 100);
// Client-side (browser)
const socket = io();
socket.on(‘stock-update’, (data) => {
updateChart(data.symbol, data.price);
});
Authentication and Authorization
Secure WebSocket connections with JWT handshake:
import jwt from 'jsonwebtoken';
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
if (!token) return next(new Error(‘Authentication required’));
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.userId = decoded.userId;
next();
} catch (err) {
next(new Error(‘Invalid token’));
}
});
io.on(‘connection’, (socket) => {
// Only authenticated users reach here
console.log(Authenticated user ${socket.userId} connected);
// Room-based authorization
socket.on(‘join-private-room’, (roomId) => {
if (hasAccess(socket.userId, roomId)) {
socket.join(roomId);
} else {
socket.emit(‘error’, ‘Unauthorized’);
}
});
});
Performance Tuning for 10k+ Connections
Node.js can handle 50k+ concurrent WebSocket connections with proper tuning:
- Increase ulimit:
ulimit -n 65535(file descriptors) - Configure socket timeouts:
pingInterval: 25000, pingTimeout: 20000 - Disable per-message deflate (compression) if CPU-constrained:
perMessageDeflate: false - Use uWebSockets.js for 2-3x better performance: Lower memory, higher throughput than ws.
import uWS from 'uWebSockets.js';
const app = uWS.App();
app.ws('/*', {
compression: uWS.DISABLED,
maxPayloadLength: 16 * 1024,
idleTimeout: 30,
open: (ws) => { console.log('Connected'); },
message: (ws, message, isBinary) => {
const msg = Buffer.from(message).toString();
ws.send(`Echo: ${msg}`);
}
});
app.listen(3000, (token) => {
if (token) console.log('uWebSockets listening on port 3000');
});
Error Handling and Graceful Shutdown
io.on('connection', (socket) => {
socket.on('error', (err) => {
console.error(`Socket ${socket.id} error:`, err);
});
socket.conn.on(‘upgrade’, (req) => {
// Handle connection upgrade errors
});
});
// Graceful shutdown
process.on(‘SIGTERM’, () => {
console.log(‘Closing WebSocket server…’);
io.close(() => {
console.log(‘WebSocket server closed’);
process.exit(0);
});
// Force close after 10 seconds
setTimeout(() => process.exit(1), 10000);
});
Common Pitfalls
- Memory leaks from event listeners: Always remove listeners on
disconnect. - Broadcasting to too many rooms:
io.to('room').emit()with 10k+ clients in one room can block event loop. Use rooms sparingly or shard. - No heartbeat mechanism: Clients may die without clean disconnect. Enable ping/pong.
- Insecure WebSocket (ws://) in production: Always use wss:// (TLS).
Alternatives to Socket.io
- uWebSockets: Raw performance (2x faster than ws), no fallback.
- ws: Minimal WebSocket implementation, use for custom protocols.
- NATS or MQTT over WebSockets: For IoT and message bus integration.
- GraphQL Subscriptions: For Apollo/GraphQL apps, less overhead for data fetching.
Conclusion
WebSockets turn your Node.js app into a real-time platform. Start with Socket.io for built-in reliability and fallback. Add Redis adapter when scaling beyond one server. Monitor connection count, message rate, and event loop lag. With proper tuning, Node.js can handle hundreds of thousands of concurrent WebSocket connections — perfect for chat, live sports scores, or collaborative tools.
Comments
Join the conversation — sign in to leave a comment.