Real-time communication is a requirement for modern applications—chat systems, live dashboards, collaborative editors, notifications, and financial tickers all demand data pushed to clients the moment it becomes available. HTTP's request-response model is insufficient for these use cases. WebSockets and Server-Sent Events (SSE) are the two primary technologies for real-time data delivery, each suited to different scenarios.

WebSockets vs. Server-Sent Events

WebSockets provide full-duplex communication over a single TCP connection. Both the client and server can send messages at any time, making WebSockets ideal for interactive applications like chat, multiplayer games, and collaborative editing where bidirectional communication is essential.

Server-Sent Events operate over standard HTTP and provide a unidirectional channel from server to client. The server pushes events to the client, but the client cannot send messages back through the same channel. SSE is simpler to implement, works seamlessly with HTTP/2 multiplexing, supports automatic reconnection, and is the right choice for dashboards, notification feeds, and live updates where the client only needs to receive data.

Implementing WebSockets with Node.js

import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';

const server = http.createServer();
const wss = new WebSocketServer({ server });

// Track connected clients by room
const rooms = new Map<string, Set<WebSocket>>();

wss.on('connection', (ws, req) => {
    const roomId = new URL(req.url!, 'http://localhost').searchParams.get('room');
    if (!roomId) { ws.close(4001, 'Room ID required'); return; }

    // Join room
    if (!rooms.has(roomId)) rooms.set(roomId, new Set());
    rooms.get(roomId)!.add(ws);

    ws.on('message', (data) => {
        const message = JSON.parse(data.toString());
        // Broadcast to all clients in the same room
        rooms.get(roomId)?.forEach(client => {
            if (client !== ws && client.readyState === WebSocket.OPEN) {
                client.send(JSON.stringify(message));
            }
        });
    });

    ws.on('close', () => {
        rooms.get(roomId)?.delete(ws);
        if (rooms.get(roomId)?.size === 0) rooms.delete(roomId);
    });
});

server.listen(8080);

Implementing Server-Sent Events

import express from 'express';

const app = express();

app.get('/events/orders', (req, res) => {
    // Set SSE headers
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.flushHeaders();

    // Send initial connection event
    res.write('event: connected\ndata: {"status":"ok"}\n\n');

    // Send periodic updates
    const interval = setInterval(() => {
        const data = JSON.stringify({
            timestamp: new Date().toISOString(),
            pendingOrders: Math.floor(Math.random() * 100)
        });
        res.write(`event: order-update\ndata: ${data}\n\n`);
    }, 5000);

    // Cleanup on client disconnect
    req.on('close', () => {
        clearInterval(interval);
        res.end();
    });
});

app.listen(3000);

Client-Side SSE

const eventSource = new EventSource('/events/orders');

eventSource.addEventListener('order-update', (event) => {
    const data = JSON.parse(event.data);
    updateDashboard(data);
});

eventSource.addEventListener('error', () => {
    // Browser automatically reconnects with SSE
    console.log('Connection lost, reconnecting...');
});

Scaling Real-Time APIs

A single server can handle tens of thousands of concurrent WebSocket connections, but horizontal scaling requires coordination. When clients connect to different server instances, messages from one instance must be propagated to clients on other instances. Redis Pub/Sub is the most common solution:

  • When a message arrives on server A, publish it to a Redis channel.
  • All server instances subscribe to the same Redis channel.
  • When a server receives a message from Redis, it broadcasts to its locally connected clients.

For SSE, the same Redis-based fan-out pattern applies. Alternatively, use a dedicated message broker like NATS or Kafka to distribute events to all server instances.

Heartbeats and Connection Health

WebSocket connections can silently die due to network changes, NAT timeouts, or proxy configurations. Implement application-level ping/pong heartbeats every 30 seconds. If a client does not respond within two heartbeat intervals, consider the connection dead and clean up resources. SSE handles this naturally through the browser's built-in reconnection mechanism with the Last-Event-ID header for resuming from the last received event.

Authentication and Security

WebSocket connections cannot carry custom headers during the handshake in browser contexts. Pass authentication tokens as query parameters or in the first message after connection. Validate tokens before accepting the connection. For SSE, standard HTTP authentication headers work since the connection is a regular HTTP request. Always use TLS (wss:// and https://) to encrypt real-time traffic.

Whether you choose WebSockets for bidirectional communication or SSE for simpler server-to-client streaming, real-time APIs require careful attention to connection management, scaling, and reliability. Both technologies are well-supported across browsers and server platforms, making them accessible for any team building real-time features.