The Real-Time Problem

HTTP was designed for request-response. Client asks, server answers, connection closes. But modern applications need real-time data flow — chat messages, live notifications, stock tickers, collaborative editing. Two dominant approaches have emerged to solve this: WebSockets and long polling. Each has trade-offs that matter in production.

Long Polling: The Pragmatic Choice

Long polling is elegant in its simplicity. The client sends a request, and the server holds it open until new data is available (or a timeout occurs). When the response arrives, the client immediately sends another request, creating a near-real-time data stream over standard HTTP.

// Server-side long polling (Express.js)
app.get('/api/messages/new', (req, res) => {
  const lastId = parseInt(req.query.after) || 0;
  const newMessages = db.getMessagesSince(room, lastId);

  if (newMessages.length > 0) {
    return res.json(newMessages);
  }

  // Hold connection open, check every 2 seconds
  const interval = setInterval(() => {
    const msgs = db.getMessagesSince(room, lastId);
    if (msgs.length > 0) {
      clearInterval(interval);
      res.json(msgs);
    }
  }, 2000);

  // Timeout after 30 seconds
  setTimeout(() => {
    clearInterval(interval);
    res.json([]);
  }, 30000);
});

Advantages: Works everywhere HTTP works. No special proxy configuration. Trivial to implement. Falls back gracefully through firewalls, load balancers, and CDNs that might strip WebSocket upgrade headers.

Disadvantages: Each poll cycle requires a new HTTP connection with full headers. Server holds open connections consuming memory. Not truly bidirectional — the client can only "push" by including data in the next poll request.

WebSockets: The Full-Duplex Solution

WebSockets establish a persistent, full-duplex connection over a single TCP socket. After an initial HTTP handshake (the "upgrade" request), both client and server can send messages at any time with minimal overhead — just 2-6 bytes of framing per message.

// Server-side WebSocket (ws library)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server });

wss.on('connection', (ws) => {
  ws.isAlive = true;

  ws.on('message', (data) => {
    // Broadcast to all connected clients
    wss.clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(data);
      }
    });
  });

  ws.on('pong', () => { ws.isAlive = true; });
});

Advantages: True real-time bidirectional communication. Minimal per-message overhead. Excellent for high-frequency updates. The server can push data instantly without waiting for a client request.

Disadvantages: Requires WebSocket-aware infrastructure. Some corporate proxies and firewalls block WebSocket connections. Reconnection logic must be handled manually. Stateful connections complicate horizontal scaling.

The Hybrid Approach

In practice, the best systems use both. WebSockets handle real-time delivery when available, while long polling serves as a reliable fallback. The client attempts a WebSocket connection first; if it fails or disconnects, it falls back to polling.

This is exactly the approach used by libraries like Socket.IO and by many production chat systems. The critical insight is that reliability matters more than latency. A message delivered via long polling in 2 seconds is infinitely better than a message lost because a WebSocket reconnect failed silently.

Scaling Considerations

Both approaches face challenges at scale. Long polling creates thundering herd problems when many clients reconnect simultaneously after a server restart. WebSockets require sticky sessions or a pub/sub layer (Redis, NATS) to broadcast across multiple server instances.

For most applications under 10,000 concurrent users, either approach works fine on a single server. Beyond that, you'll need a message broker regardless of your transport choice. The protocol becomes less important than your overall architecture.

Choose long polling when simplicity and compatibility matter most. Choose WebSockets when you need sub-100ms latency and true bidirectional streaming. Choose both when you're building something that needs to work everywhere, every time.