Performance

WebSockets vs Polling

Persistent bidirectional connections versus repeated short-lived requests, knowing which fits.

Overview

Short polling, long polling, Server-Sent Events (SSE), and WebSockets are four mechanisms for real-time data delivery from server to client. Short polling requests the server on an interval; long polling holds the connection until the server has new data. SSE provides a one-way persistent server-to-client stream. WebSockets provide full-duplex bidirectional communication. The choice depends on directionality, latency requirements, and infrastructure constraints.

Origin

Short polling was the earliest real-time technique, used in early Ajax applications (2005). Long polling (Comet, Alex Russell, 2006) reduced wasted requests. Server-Sent Events were introduced in the HTML5 specification (2009) and are now in the WHATWG Living Standard. WebSocket protocol was standardised as RFC 6455 (2011). Socket.IO (2010) abstracted over all four mechanisms with automatic fallback.

Examples

WebSocket server and client implementation in TypeScript

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

interface ClientMessage {
  type: 'subscribe' | 'unsubscribe';
  channel: string;
}

const server = http.createServer();
const wss = new WebSocketServer({ server });
const channels = new Map<string, Set<WebSocket>>();

wss.on('connection', (ws: WebSocket, req) => {
  // Authenticate via token in query string (headers not accessible on WS upgrade)
  const token = new URL(req.url!, 'ws://localhost').searchParams.get('token');
  if (!verifyToken(token)) { ws.close(1008, 'Unauthorized'); return; }

  ws.on('message', (data) => {
    const msg: ClientMessage = JSON.parse(data.toString());
    if (msg.type === 'subscribe') {
      const subscribers = channels.get(msg.channel) ?? new Set();
      subscribers.add(ws);
      channels.set(msg.channel, subscribers);
    }
  });

  ws.on('close', () => {
    channels.forEach(subs => subs.delete(ws)); // Cleanup all subscriptions
  });
});

// Broadcast to all subscribers of a channel
function broadcast(channel: string, payload: unknown): void {
  const subscribers = channels.get(channel) ?? new Set();
  const message = JSON.stringify(payload);
  subscribers.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

server.listen(3001);

WebSocket connections are stateful; the server must track subscribers and clean up on close. In a multi-server deployment, subscriptions must be coordinated via Redis Pub/Sub (using ioredis publish/subscribe) so a message broadcast on server A reaches clients connected to server B.

Server-Sent Events for one-way push in TypeScript

import { Request, Response } from 'express';

export function sseEndpoint(req: Request, res: Response): void {
  // SSE requires specific headers
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.flushHeaders(); // Send headers immediately; begin stream

  // Send a heartbeat every 30s to prevent proxy/firewall timeouts
  const heartbeat = setInterval(() => {
    res.write(': heartbeat

'); // Comment line in SSE protocol
  }, 30000);

  // Subscribe to events and push to client
  function sendEvent(eventType: string, data: unknown) {
    res.write('event: ' + eventType + '
');
    res.write('data: ' + JSON.stringify(data) + '

');
  }

  const subscription = eventBus.on('order.status.changed', (event) => {
    if (event.userId === (req as any).user.sub) {
      sendEvent('order-update', event);
    }
  });

  req.on('close', () => {
    clearInterval(heartbeat);
    subscription.unsubscribe();
  });
}

SSE is simpler than WebSockets for server-to-client-only push: it uses plain HTTP, works through proxies without special configuration, reconnects automatically via the browser EventSource API, and supports event IDs for resuming after disconnection. Use WebSockets only when bidirectional communication is needed.

Use Cases

  • 01WebSockets: collaborative editing, multiplayer games, chat applications where both client and server send messages frequently
  • 02SSE: live dashboards, notifications, order status updates, CI/CD build logs where the server pushes data and clients only read
  • 03Long polling: environments where WebSockets are blocked by firewalls or proxies; compatible with standard HTTP infrastructure
  • 04Short polling: extremely simple implementations for low-frequency updates (every 30+ seconds) where simplicity outweighs efficiency

When Not to Use

  • //Do not use WebSockets for simple one-way notifications; SSE achieves the same result with less server complexity and automatic browser reconnection
  • //Do not use short polling for low-latency updates; frequent polls waste bandwidth and server resources compared to a persistent connection
  • //Do not use SSE when the client needs to send data back frequently; SSE is HTTP unidirectional. Use WebSockets or separate HTTP POST calls instead

Technical Notes

  • WebSocket connections bypass HTTP/2 multiplexing; each WebSocket is a separate TCP connection (or a single HTTP/2 Extended CONNECT tunnel in h2c). At scale, many WebSocket connections require careful file descriptor management
  • AWS API Gateway WebSocket API manages WebSocket connections as a managed service; connection IDs are routable, allowing any backend instance to push to any connected client via the Management API
  • Socket.IO (v4) uses WebSockets with an HTTP long-polling fallback and adds rooms, namespaces, and acknowledgements on top. In 2024, it requires the socket.io-adapter-redis package for multi-server deployments using Redis Pub/Sub
  • The SSE EventSource API in the browser automatically reconnects after disconnection (3 seconds default) and sends the Last-Event-ID header, allowing the server to resume the stream from the last delivered event, making SSE resilient to network interruptions