Client Server
Backend

Building Real-Time Apps with WebSockets

Mayur Dabhi
Mayur Dabhi
Mar 8, 2026
22 min read

You add a chat widget or a notification bell, wire it up with a setInterval that hits /api/messages every five seconds, and it works. Then you look at the server logs and see that almost every one of those requests came back empty, messages still show up a few seconds late, and the request count climbs with every open tab. That's the point where WebSockets start making sense: one long-lived connection that either side can send on whenever it wants.

This post goes through how the protocol works, the native browser API with a small ws server, then Socket.io for rooms, a chat server, notifications, scaling with Redis, security, reconnection and testing. I'll finish with when I'd reach for WebSockets and when I wouldn't.

What's Covered
  • How WebSockets differ from traditional HTTP
  • The WebSocket handshake and protocol mechanics
  • Building real-time apps with native WebSocket API
  • Socket.io: rooms, reconnection and multi-server setups
  • Common patterns: chat, notifications, live dashboards
  • Security, reconnection and testing

Why Plain HTTP Struggles With Real-Time

HTTP is request and response. The server can't speak until the client asks, so "push" has to be faked:

Traditional HTTP
Client → Request → Server
Client ← Response ← Server
// Connection closes

// For updates:
setInterval(() => {
  fetch('/api/messages')
}, 5000); // Polling
vs
WebSockets
Client ↔ Server
// Persistent connection!

// Instant updates:
socket.on('message', (data) => {
  // Received immediately
  displayMessage(data);
});

The Problems with Polling

Before WebSockets were widely supported, these were the usual workarounds. They still have their place:

Technique How It Works Drawbacks
Short Polling Client repeatedly asks server for updates High latency, wasted requests, server load
Long Polling Server holds request until data available Connection overhead, timeout management
Server-Sent Events One-way server → client stream Client sends data with normal HTTP requests; fine for feeds and dashboards
WebSockets Full-duplex bidirectional connection Stateful connections are harder to scale and proxy

How WebSockets Work

The connection starts life as an ordinary HTTP request. Once the server agrees to upgrade, the same TCP connection stays open and either side can send frames at any time.

Client Server HTTP Upgrade Request 101 Switching Protocols Full-Duplex Channel 1 2 3

WebSocket connection establishment: HTTP upgrade handshake followed by persistent bidirectional communication

The WebSocket Handshake

The handshake is three steps:

1. Client Sends Upgrade Request

The client sends a special HTTP request with Upgrade: websocket header and a unique key.

2. Server Responds with 101

If the server supports WebSockets, it responds with 101 Switching Protocols and an accept key.

3. Connection Established

The TCP connection upgrades to WebSocket. Both parties can now send messages freely.

WebSocket Handshake Headers
// Client Request
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

// Server Response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Native WebSocket API in JavaScript

Every current browser ships a WebSocket class, so you don't need a library to get started. It's worth writing a client by hand once so you know what libraries are doing for you:

client.js - Browser WebSocket
// Create WebSocket connection
const socket = new WebSocket('ws://localhost:8080');

// Connection opened
socket.addEventListener('open', (event) => {
    console.log('Connected to server!');
    socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', (event) => {
    console.log('Message from server:', event.data);
});

// Handle errors
socket.addEventListener('error', (event) => {
    console.error('WebSocket error:', event);
});

// Connection closed
socket.addEventListener('close', (event) => {
    console.log('Disconnected from server');
    if (event.wasClean) {
        console.log(`Code: ${event.code}, Reason: ${event.reason}`);
    } else {
        console.log('Connection died unexpectedly');
    }
});

// Send data
function sendMessage(message) {
    if (socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify({ type: 'chat', data: message }));
    }
}

Creating a WebSocket Server with Node.js

On the server, ws is the usual choice in Node. It's small and doesn't add its own protocol on top:

server.js - Node.js WebSocket Server
const WebSocket = require('ws');

// Create WebSocket server on port 8080
const wss = new WebSocket.Server({ port: 8080 });

// Store connected clients
const clients = new Set();

wss.on('connection', (ws, req) => {
    const clientIP = req.socket.remoteAddress;
    console.log(`New client connected: ${clientIP}`);
    
    // Add to clients set
    clients.add(ws);
    
    // Send welcome message
    ws.send(JSON.stringify({
        type: 'welcome',
        message: 'Connected to WebSocket server!'
    }));

    // Handle incoming messages
    ws.on('message', (data) => {
        try {
            const message = JSON.parse(data);
            console.log('Received:', message);
            
            // Broadcast to all clients
            broadcast(message, ws);
        } catch (e) {
            console.error('Invalid JSON:', e);
        }
    });

    // Handle client disconnect
    ws.on('close', () => {
        console.log('Client disconnected');
        clients.delete(ws);
    });

    // Handle errors
    ws.on('error', (error) => {
        console.error('WebSocket error:', error);
    });
});

// Broadcast message to all clients except sender
function broadcast(message, sender) {
    clients.forEach((client) => {
        if (client !== sender && client.readyState === WebSocket.OPEN) {
            client.send(JSON.stringify(message));
        }
    });
}

console.log('WebSocket server running on ws://localhost:8080');
WebSocket States

readyState is one of four values. Calling send() while it's still CONNECTING throws an InvalidStateError, which is why the client above checks for OPEN first.

  • CONNECTING (0): handshake in progress
  • OPEN (1): ready to send and receive
  • CLOSING (2): close handshake started
  • CLOSED (3): closed or never opened
Behind Nginx? Forward the Upgrade Headers

The most common "works locally, fails in production" problem: Nginx doesn't pass Upgrade and Connection through by default, so the handshake never completes and the browser logs WebSocket connection to 'wss://...' failed. Add proxy_http_version 1.1;, proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection "upgrade"; to the location block. Also note that proxy_read_timeout defaults to 60 seconds, so an idle connection with no ping traffic will get closed by the proxy.

Scaling with Socket.io

Raw WebSockets give you a pipe and nothing else. Reconnection, grouping clients, and acknowledgements are all on you. Socket.io adds those on top. One thing to know up front: it uses its own protocol, so a Socket.io client can't talk to a plain ws server and vice versa.

Automatic Reconnection

Retries dropped connections with exponential backoff.

Room Support

Group clients into rooms and broadcast to just that group.

Fallback Transports

Falls back to long-polling if WebSockets aren't available.

Namespace Support

Multiplex connections over a single socket with namespaces.

Setting Up Socket.io

Terminal
# Install Socket.io for server
npm install socket.io

# Install Socket.io client (for Node.js clients)
npm install socket.io-client
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
    cors: {
        origin: "http://localhost:3000",
        methods: ["GET", "POST"]
    }
});

// Connection handler
io.on('connection', (socket) => {
    console.log('User connected:', socket.id);

    // Join a room
    socket.on('join-room', (roomId) => {
        socket.join(roomId);
        console.log(`User ${socket.id} joined room ${roomId}`);
        
        // Notify others in the room
        socket.to(roomId).emit('user-joined', {
            userId: socket.id,
            message: 'A new user joined the room'
        });
    });

    // Handle chat messages
    socket.on('chat-message', (data) => {
        // Emit to specific room
        io.to(data.room).emit('new-message', {
            userId: socket.id,
            message: data.message,
            timestamp: new Date().toISOString()
        });
    });

    // Handle typing indicator
    socket.on('typing', (data) => {
        socket.to(data.room).emit('user-typing', {
            userId: socket.id
        });
    });

    // Handle disconnect
    socket.on('disconnect', () => {
        console.log('User disconnected:', socket.id);
    });
});

httpServer.listen(3001, () => {
    console.log('Socket.io server running on port 3001');
});
<!-- Include Socket.io client -->
<script src="/socket.io/socket.io.js"></script>

<script>
// Connect to server
const socket = io('http://localhost:3001');

// Connection events
socket.on('connect', () => {
    console.log('Connected:', socket.id);
    
    // Join a chat room
    socket.emit('join-room', 'general');
});

// Listen for new messages
socket.on('new-message', (data) => {
    displayMessage(data);
});

// Listen for typing indicator
socket.on('user-typing', (data) => {
    showTypingIndicator(data.userId);
});

// Send a message
function sendMessage(message) {
    socket.emit('chat-message', {
        room: 'general',
        message: message
    });
}

// Emit typing event
function onTyping() {
    socket.emit('typing', { room: 'general' });
}

// Handle disconnection
socket.on('disconnect', () => {
    console.log('Disconnected from server');
});

// Reconnection handling
socket.on('reconnect', (attemptNumber) => {
    console.log(`Reconnected after ${attemptNumber} attempts`);
});
</script>

Building a Real-Time Chat Application

Here's a fuller chat server with rooms, presence and a short message history. Storage is in memory to keep it readable, so it forgets everything on restart:

React/Vue Client Mobile App Web Browser
↕
Socket.io Server Room Manager Event Handler
↕
Redis Pub/Sub MongoDB Auth Service
chat-server.js - Complete Chat Server
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer);

// In-memory storage (use Redis/DB in production)
const rooms = new Map();
const users = new Map();

io.on('connection', (socket) => {
    console.log(`Client connected: ${socket.id}`);

    // User authentication
    socket.on('authenticate', (userData) => {
        const user = {
            id: socket.id,
            name: userData.name,
            avatar: userData.avatar || getDefaultAvatar(userData.name)
        };
        users.set(socket.id, user);
        socket.emit('authenticated', user);
    });

    // Join a chat room
    socket.on('join-room', (roomId) => {
        const user = users.get(socket.id);
        if (!user) return socket.emit('error', 'Not authenticated');

        socket.join(roomId);
        
        // Initialize room if not exists
        if (!rooms.has(roomId)) {
            rooms.set(roomId, { 
                messages: [], 
                users: new Set() 
            });
        }
        
        const room = rooms.get(roomId);
        room.users.add(socket.id);

        // Send room history
        socket.emit('room-history', room.messages.slice(-50));

        // Broadcast user joined
        io.to(roomId).emit('user-presence', {
            type: 'joined',
            user: user,
            onlineCount: room.users.size
        });
    });

    // Handle new message
    socket.on('send-message', ({ roomId, content }) => {
        const user = users.get(socket.id);
        if (!user) return;

        const message = {
            id: generateId(),
            userId: socket.id,
            userName: user.name,
            avatar: user.avatar,
            content: content,
            timestamp: new Date().toISOString()
        };

        // Store message
        const room = rooms.get(roomId);
        if (room) {
            room.messages.push(message);
            // Keep last 100 messages
            if (room.messages.length > 100) {
                room.messages.shift();
            }
        }

        // Broadcast to room
        io.to(roomId).emit('new-message', message);
    });

    // Typing indicator
    let typingTimeout;
    socket.on('typing', ({ roomId }) => {
        const user = users.get(socket.id);
        if (!user) return;

        socket.to(roomId).emit('user-typing', {
            userId: socket.id,
            userName: user.name
        });

        // Auto-clear typing after 3 seconds
        clearTimeout(typingTimeout);
        typingTimeout = setTimeout(() => {
            socket.to(roomId).emit('user-stopped-typing', {
                userId: socket.id
            });
        }, 3000);
    });

    // Leave room
    socket.on('leave-room', (roomId) => {
        handleLeaveRoom(socket, roomId);
    });

    // Disconnect
    socket.on('disconnect', () => {
        const user = users.get(socket.id);
        
        // Leave all rooms
        rooms.forEach((room, roomId) => {
            if (room.users.has(socket.id)) {
                handleLeaveRoom(socket, roomId);
            }
        });

        users.delete(socket.id);
        console.log(`Client disconnected: ${socket.id}`);
    });
});

function handleLeaveRoom(socket, roomId) {
    const user = users.get(socket.id);
    const room = rooms.get(roomId);
    
    if (room && user) {
        room.users.delete(socket.id);
        socket.leave(roomId);
        
        io.to(roomId).emit('user-presence', {
            type: 'left',
            user: user,
            onlineCount: room.users.size
        });

        // Clean up empty rooms
        if (room.users.size === 0) {
            rooms.delete(roomId);
        }
    }
}

function generateId() {
    return Math.random().toString(36).substr(2, 9);
}

function getDefaultAvatar(name) {
    return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}`;
}

httpServer.listen(3001, () => {
    console.log('Chat server running on http://localhost:3001');
});

Common Real-Time Use Cases

The patterns I see most often:

Chat Applications

Instant messaging, group chats, customer support widgets

Live Notifications

Social media alerts, email notifications, activity feeds

Live Dashboards

Stock tickers, analytics, monitoring systems

Multiplayer Games

Real-time game state, player positions, game events

Collaborative Editing

Google Docs-style real-time document editing

Location Tracking

Delivery tracking, ride-sharing, fleet management

Live Notifications System

notifications.js - Real-Time Notification System
// Server-side notification service
class NotificationService {
    constructor(io) {
        this.io = io;
        this.userSockets = new Map(); // userId -> Set(socketIds)
    }

    // Register user socket
    registerUser(userId, socketId) {
        if (!this.userSockets.has(userId)) {
            this.userSockets.set(userId, new Set());
        }
        this.userSockets.get(userId).add(socketId);
    }

    // Unregister user socket
    unregisterUser(userId, socketId) {
        const sockets = this.userSockets.get(userId);
        if (sockets) {
            sockets.delete(socketId);
            if (sockets.size === 0) {
                this.userSockets.delete(userId);
            }
        }
    }

    // Send notification to specific user
    sendToUser(userId, notification) {
        const sockets = this.userSockets.get(userId);
        if (sockets) {
            const data = {
                id: generateId(),
                ...notification,
                timestamp: new Date().toISOString(),
                read: false
            };
            
            sockets.forEach(socketId => {
                this.io.to(socketId).emit('notification', data);
            });
        }
    }

    // Send notification to multiple users
    sendToUsers(userIds, notification) {
        userIds.forEach(userId => {
            this.sendToUser(userId, notification);
        });
    }

    // Broadcast to all connected users
    broadcast(notification) {
        this.io.emit('notification', {
            id: generateId(),
            ...notification,
            timestamp: new Date().toISOString()
        });
    }
}

// Usage with Socket.io
const notificationService = new NotificationService(io);

io.on('connection', (socket) => {
    socket.on('register', (userId) => {
        notificationService.registerUser(userId, socket.id);
    });

    socket.on('disconnect', () => {
        // Find and unregister user
        notificationService.userSockets.forEach((sockets, userId) => {
            if (sockets.has(socket.id)) {
                notificationService.unregisterUser(userId, socket.id);
            }
        });
    });
});

// Example: Send notification when someone follows a user
app.post('/api/follow', async (req, res) => {
    const { followerId, followedId } = req.body;
    
    // Save to database
    await saveFollow(followerId, followedId);
    
    // Send real-time notification
    const follower = await getUser(followerId);
    notificationService.sendToUser(followedId, {
        type: 'follow',
        title: 'New Follower',
        message: `${follower.name} started following you`,
        avatar: follower.avatar,
        link: `/profile/${followerId}`
    });
    
    res.json({ success: true });
});

Scaling WebSocket Applications

Everything above assumes one Node process. As soon as you run two (PM2 cluster mode counts), it breaks in a confusing way: some users get messages and some don't.

Scaling Challenge

WebSocket connections are stateful. If you have multiple servers, a message from User A on Server 1 won't reach User B on Server 2 unless something relays it. With Socket.io you also need sticky sessions at the load balancer, because the HTTP long-polling fallback sends several requests that must all land on the same server. Without them you'll see repeated 400 Bad Request responses with "Session ID unknown".

Using Redis Adapter for Socket.io

scaled-server.js - Multi-Server Setup with Redis
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const io = new Server(httpServer);

// Create Redis clients
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

// Wait for Redis connection
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
    // Attach Redis adapter
    io.adapter(createAdapter(pubClient, subClient));
    
    console.log('Socket.io with Redis adapter ready');
});

// Now events are broadcast across all server instances
io.on('connection', (socket) => {
    socket.on('chat-message', (data) => {
        // This will reach all clients on ALL servers
        io.to(data.room).emit('new-message', data);
    });
});
Load Balancer Server 1 Server 2 Server 3 Redis Pub/Sub

Horizontal scaling with Redis adapter enables cross-server communication

Security Best Practices

A WebSocket endpoint is an API endpoint that stays open, so it needs the same care as your REST routes. Browsers don't apply CORS to WebSocket connections the way they do to fetch, so check the Origin header yourself if you rely on cookies.

WebSocket Security Checklist

  • Always use wss:// (WebSocket Secure) in production
  • Validate and authenticate connections on handshake
  • Implement rate limiting to prevent abuse
  • Sanitize all incoming messages to prevent XSS
  • Set appropriate CORS policies
  • Implement heartbeat/ping-pong for connection health
  • Use JWT or session tokens for authentication
  • Implement message size limits
  • Log and monitor WebSocket connections
secure-server.js - Security Implementation
const { Server } = require('socket.io');
const jwt = require('jsonwebtoken');
const rateLimit = require('./rateLimiter');

const io = new Server(httpServer, {
    cors: {
        origin: ['https://yourdomain.com'],
        credentials: true
    },
    // Connection timeout
    pingTimeout: 60000,
    pingInterval: 25000,
    // Max payload size (1MB)
    maxHttpBufferSize: 1e6
});

// Authentication middleware
io.use(async (socket, next) => {
    try {
        const token = socket.handshake.auth.token;
        
        if (!token) {
            return next(new Error('Authentication required'));
        }

        // Verify JWT token
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        
        // Attach user to socket
        socket.user = decoded;
        
        // Check if user is banned/disabled
        const user = await getUserById(decoded.id);
        if (!user || user.banned) {
            return next(new Error('User not authorized'));
        }

        next();
    } catch (error) {
        next(new Error('Invalid token'));
    }
});

// Rate limiting middleware
io.use((socket, next) => {
    const clientIP = socket.handshake.address;
    
    if (rateLimit.isLimited(clientIP)) {
        return next(new Error('Too many connections'));
    }
    
    rateLimit.increment(clientIP);
    next();
});

io.on('connection', (socket) => {
    // Message validation and sanitization
    socket.on('message', (data) => {
        // Validate message structure
        if (!isValidMessage(data)) {
            return socket.emit('error', 'Invalid message format');
        }

        // Sanitize content
        const sanitized = sanitizeHtml(data.content, {
            allowedTags: [],
            allowedAttributes: {}
        });

        // Size check
        if (sanitized.length > 5000) {
            return socket.emit('error', 'Message too long');
        }

        // Process sanitized message
        processMessage(socket, { ...data, content: sanitized });
    });
});

function isValidMessage(data) {
    return (
        typeof data === 'object' &&
        typeof data.room === 'string' &&
        typeof data.content === 'string' &&
        data.room.length > 0 &&
        data.room.length <= 50
    );
}

Handling Connection Issues

Phones switch networks, laptops sleep, proxies time out idle connections. If you use the native API you have to write reconnection yourself. Add jitter to the delay so that when a server restarts, all clients don't reconnect at the same instant:

reconnection.js - Client-Side Reconnection
class ReconnectingWebSocket {
    constructor(url, options = {}) {
        this.url = url;
        this.options = {
            maxRetries: 10,
            initialDelay: 1000,
            maxDelay: 30000,
            factor: 2,
            ...options
        };
        
        this.retries = 0;
        this.listeners = new Map();
        this.connect();
    }

    connect() {
        this.ws = new WebSocket(this.url);
        
        this.ws.onopen = () => {
            console.log('Connected');
            this.retries = 0;
            this.emit('connected');
        };

        this.ws.onmessage = (event) => {
            const data = JSON.parse(event.data);
            this.emit('message', data);
        };

        this.ws.onclose = (event) => {
            if (!event.wasClean) {
                this.reconnect();
            }
        };

        this.ws.onerror = (error) => {
            console.error('WebSocket error:', error);
        };
    }

    reconnect() {
        if (this.retries >= this.options.maxRetries) {
            this.emit('maxRetriesReached');
            return;
        }

        // Exponential backoff with jitter
        const delay = Math.min(
            this.options.initialDelay * Math.pow(this.options.factor, this.retries),
            this.options.maxDelay
        ) * (0.5 + Math.random() * 0.5);

        console.log(`Reconnecting in ${Math.round(delay)}ms...`);
        this.emit('reconnecting', { attempt: this.retries + 1, delay });

        setTimeout(() => {
            this.retries++;
            this.connect();
        }, delay);
    }

    send(data) {
        if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(data));
        } else {
            console.warn('Cannot send: WebSocket not open');
        }
    }

    on(event, callback) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, []);
        }
        this.listeners.get(event).push(callback);
    }

    emit(event, data) {
        const callbacks = this.listeners.get(event) || [];
        callbacks.forEach(cb => cb(data));
    }
}

// Usage
const ws = new ReconnectingWebSocket('wss://api.example.com/ws');

ws.on('connected', () => {
    console.log('Ready to send messages');
});

ws.on('message', (data) => {
    console.log('Received:', data);
});

ws.on('reconnecting', ({ attempt, delay }) => {
    showNotification(`Connection lost. Reconnecting (attempt ${attempt})...`);
});

Testing WebSocket Applications

Testing Tools
  • wscat: Command-line WebSocket client (npm install -g wscat)
  • Postman: Supports WebSocket testing in recent versions
  • Browser DevTools: Network tab → WS filter
  • socket.io-client: For automated testing with Jest/Mocha
websocket.test.js - Testing with Jest
const { createServer } = require('http');
const { Server } = require('socket.io');
const Client = require('socket.io-client');

describe('WebSocket Server', () => {
    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 receive message from client', (done) => {
        serverSocket.on('chat', (message) => {
            expect(message).toBe('Hello Server');
            done();
        });
        
        clientSocket.emit('chat', 'Hello Server');
    });

    test('should broadcast message to all clients', (done) => {
        clientSocket.on('broadcast', (message) => {
            expect(message).toBe('Hello Everyone');
            done();
        });
        
        serverSocket.emit('broadcast', 'Hello Everyone');
    });

    test('should join room and receive room messages', (done) => {
        clientSocket.emit('join', 'test-room');
        
        clientSocket.on('room-message', (data) => {
            expect(data.room).toBe('test-room');
            expect(data.message).toBe('Welcome!');
            done();
        });

        serverSocket.on('join', (room) => {
            serverSocket.join(room);
            io.to(room).emit('room-message', {
                room: room,
                message: 'Welcome!'
            });
        });
    });
});

Performance Optimization Tips

Message Compression

perMessageDeflate compresses frames and cuts bandwidth, but it costs CPU and memory per connection. Test it under load before turning it on.

Selective Broadcasting

Use rooms and namespaces to send messages only to relevant clients.

Binary Data

Send binary data instead of JSON for large payloads like images or files.

Connection Pooling

Limit connections per user and implement proper cleanup on disconnect.

When I'd Use WebSockets (and When I Wouldn't)

I reach for WebSockets when both sides genuinely talk to each other often: chat, collaborative editing, multiplayer state, anything with typing indicators or presence. In those cases polling is either too slow or too wasteful, and SSE leaves you sending every client action as a separate HTTP request.

I wouldn't use them for things that only flow one way. A notification bell, a build log, a live order status: Server-Sent Events handle those over plain HTTP, reconnect on their own, and go through most proxies and CDNs without special config. If updates arrive every minute or so, a 30 second poll is boring and perfectly fine, and it scales like any other endpoint.

What to Check Before Going to Production

  • Your reverse proxy forwards Upgrade/Connection and its read timeout is longer than your ping interval
  • Sticky sessions are on if you run more than one Socket.io instance, plus the Redis adapter
  • Authentication happens during the handshake, not in the first message
  • Clients refetch missed data after a reconnect; messages sent while offline are simply lost
  • You have a limit on open file descriptors (ulimit -n) high enough for the number of connections you expect
  • Deploys drain connections gracefully, or you've accepted that every deploy disconnects everyone at once
WebSockets Real-time Node.js Socket.io Chat Application Live Updates
Mayur Dabhi

Mayur Dabhi

Full Stack Developer specializing in Laravel, React, and Node.js. Passionate about building scalable web applications and sharing knowledge with the developer community.