WebSockets vs Server-Sent Events vs Long Polling: Real-Time Communication Explained

WebSockets vs Server-Sent Events vs Long Polling: Real-Time Communication Explained

Real-time features are now expected in every web application — live notifications, chat, dashboards, collaborative editing. Three techniques deliver this: WebSockets (bidirectional), Server-Sent Events (server-to-client only), and Long Polling (HTTP-based fallback). Choosing the right one determines your scalability, complexity, and infrastructure costs.

TL;DR: WebSockets for bidirectional real-time (chat, gaming, collaborative editing). SSE for server-to-client streams (notifications, live feeds, AI streaming responses). Long Polling for simple real-time on infrastructure that blocks WebSockets. SSE is underrated — it is simpler than WebSockets for one-directional data.

Long Polling — the HTTP fallback

// Client: make request, server holds it until data is available or timeout
// Then client immediately makes another request

// Server (Express):
app.get('/poll', async (req, res) => {
  const timeout = 30000; // 30s max hold
  const deadline = Date.now() + timeout;

  while (Date.now() < deadline) {
    const event = await checkForNewEvents(req.user.id);
    if (event) return res.json(event);
    await sleep(500); // Check every 500ms
  }

  res.json({ type: 'timeout' }); // No event in 30s
});

// Client:
async function poll() {
  while (true) {
    try {
      const event = await fetch('/poll').then(r => r.json());
      if (event.type !== 'timeout') handleEvent(event);
    } catch (e) { await sleep(2000); } // Retry on error
  }
}
// Pros: works everywhere, no special infrastructure
// Cons: slow (500ms-1s latency), inefficient (constant connections)

Server-Sent Events — the underrated choice

// SSE: server pushes events over persistent HTTP connection
// One-directional: server → client only
// Built-in reconnection, event IDs, named events

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

  // Send events
  const sendEvent = (data, event = 'message', id = Date.now()) => {
    res.write(`id: ${id}\n`);
    res.write(`event: ${event}\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  sendEvent({ msg: 'Connected!' }, 'connected');

  // Subscribe to events for this user
  const unsubscribe = eventBus.subscribe(req.user.id, (event) => {
    sendEvent(event, event.type);
  });

  // Cleanup on disconnect
  req.on('close', () => unsubscribe());
});

// Client: built-in browser API with automatic reconnection
const es = new EventSource('/events');
es.addEventListener('notification', (e) => {
  const data = JSON.parse(e.data);
  showNotification(data);
});
es.addEventListener('error', () => {
  // Auto-reconnects after 3 seconds by default
});

// SSE advantages over WebSockets for one-directional:
// - Uses regular HTTP (works through proxies, load balancers)
// - Built-in reconnection with last event ID
// - Much simpler to implement and scale

WebSockets — bidirectional real-time

// WebSockets: persistent TCP-like connection over HTTP upgrade
// True bidirectional: client ↔ server

// Server (Node.js with ws library):
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8080 });

const rooms = new Map();

wss.on('connection', (ws, req) => {
  const userId = authenticateUser(req);
  ws.userId = userId;

  ws.on('message', (raw) => {
    const msg = JSON.parse(raw);
    switch (msg.type) {
      case 'join_room':
        if (!rooms.has(msg.roomId)) rooms.set(msg.roomId, new Set());
        rooms.get(msg.roomId).add(ws);
        break;
      case 'chat':
        // Broadcast to room
        rooms.get(msg.roomId)?.forEach(client => {
          if (client !== ws && client.readyState === 1) {
            client.send(JSON.stringify({ type: 'chat', msg: msg.text, from: userId }));
          }
        });
    }
  });

  ws.on('close', () => {
    rooms.forEach(room => room.delete(ws));
  });
});

// Client:
const ws = new WebSocket('wss://api.example.com/ws');
ws.onopen = () => ws.send(JSON.stringify({ type: 'join_room', roomId: 'general' }));
ws.onmessage = (e) => handleMessage(JSON.parse(e.data));
ws.onclose = () => setTimeout(reconnect, 1000); // Manual reconnect

Decision guide

  • SSE: notifications, live feeds, AI token streaming, dashboards — simple and scalable
  • WebSockets: chat, collaborative editing, multiplayer gaming, live code collaboration
  • Long Polling: environments that block WebSockets, simple status updates
  • ❌ Don’t use WebSockets when SSE works — WebSockets need sticky sessions or Redis pub/sub for horizontal scaling
  • ❌ Don’t use Long Polling if SSE or WebSockets are available — 10x more server connections

WebSocket scaling is a system design concern — the rate limiter guide covers how to throttle WebSocket message rates. For Lambda streaming (SSE equivalent in serverless), see the Lambda Function URL streaming guide. External reference: MDN Server-Sent Events documentation.

Recommended Reading

Designing Data-Intensive Applications — The essential book every senior developer needs. Covers distributed systems, databases, and production architecture.

The Pragmatic Programmer — Timeless engineering wisdom for writing better, more maintainable code at any level.

Affiliate links. We earn a small commission at no extra cost to you.

Free Weekly Newsletter

🚀 Don’t Miss the Next Cheat Code

Join 1,000+ senior developers getting expert-level JS, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.

✓ No spam✓ Unsubscribe anytime✓ Expert-level only

Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply