DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

WebSockets vs Server-Sent Events vs Polling: When to Use Which

Real-time features — chat, live notifications, collaborative editing, live dashboards — all require the server to push data to the client. There are three main approaches: WebSockets, Server-Sent Events (SSE), and polling. Picking the wrong one leads to either unnecessary complexity or something that doesn't scale.

Polling

The simplest approach: the client asks the server for updates on a schedule.

// Client
setInterval(async () => {
  const response = await fetch('/api/notifications/new?since=' + lastCheck);
  const data = await response.json();
  if (data.notifications.length > 0) {
    updateUI(data.notifications);
    lastCheck = Date.now();
  }
}, 3000);  // check every 3 seconds
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Works with any HTTP server
  • Simple to implement and debug
  • No special infrastructure
  • Works behind all proxies and firewalls

Cons:

  • Latency = polling interval (up to 3 seconds in the example above)
  • Wasteful: most polls return nothing
  • Generates constant load even when idle

Long polling reduces waste by holding the connection open until data is available:

async function longPoll() {
  while (true) {
    try {
      const response = await fetch('/api/events/wait');  // server holds 30s
      const data = await response.json();
      handleEvent(data);
    } catch {
      await new Promise(r => setTimeout(r, 5000));  // retry after error
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
// Server (Express)
app.get('/api/events/wait', async (req, res) => {
  const event = await waitForEvent(req.user.id, { timeout: 30000 });
  if (event) {
    res.json(event);
  } else {
    res.json({ type: 'timeout' });
  }
});
Enter fullscreen mode Exit fullscreen mode

Long polling is a reasonable choice when you need real-time but can't use WebSockets (corporate proxies, simple infrastructure).

Server-Sent Events (SSE)

SSE is a simple protocol for one-way server-to-client streaming over HTTP. The browser keeps a persistent HTTP connection open; the server sends events as text:

// Client
const source = new EventSource('/api/stream');

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  updateUI(data);
});

source.addEventListener('notification', (e) => {
  showNotification(JSON.parse(e.data));
});

source.addEventListener('error', () => {
  console.log('Connection lost, will auto-reconnect');
});

// EventSource automatically reconnects on disconnect
source.close();  // explicit close
Enter fullscreen mode Exit fullscreen mode
// Server (Express)
app.get('/api/stream', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'X-Accel-Buffering': 'no'  // disable nginx buffering
  });

  const send = (event, data) => {
    res.write(`event: ${event}\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  // Send initial data
  send('message', { type: 'connected', userId: req.user.id });

  // Subscribe to updates
  const unsubscribe = notificationBus.subscribe(req.user.id, (event) => {
    send('notification', event);
  });

  // Cleanup on disconnect
  req.on('close', () => {
    unsubscribe();
  });
});
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Simpler than WebSockets (standard HTTP, easy to debug in Network panel)
  • Automatic reconnection built into the browser
  • Works with HTTP/2 multiplexing
  • Works behind most proxies (it's just HTTP)

Cons:

  • One-way only (server → client)
  • HTTP/1.1 limited to 6 connections per domain (HTTP/2 removes this)
  • Not supported in older IE (but 97%+ browser support now)

Best for: Notifications, live feeds, progress updates, AI streaming responses (ChatGPT-style).

WebSockets

WebSockets provide full-duplex, persistent connections between client and server. Both sides can send messages at any time.

// Client
const ws = new WebSocket('wss://api.example.com/ws');

ws.addEventListener('open', () => {
  ws.send(JSON.stringify({ type: 'subscribe', channel: 'room-42' }));
});

ws.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  handleMessage(data);
});

ws.addEventListener('close', (e) => {
  console.log('Closed:', e.code, e.reason);
  // Implement reconnection logic
});

ws.addEventListener('error', () => {
  console.error('WebSocket error');
});

// Send a message
ws.send(JSON.stringify({ type: 'chat', text: 'Hello!' }));
Enter fullscreen mode Exit fullscreen mode
// Server (Node.js with 'ws' library)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

const rooms = new Map();

wss.on('connection', (ws, req) => {
  ws.userId = authenticate(req);  // extract from cookie/token
  let currentRoom = null;

  ws.on('message', (message) => {
    const data = JSON.parse(message);

    if (data.type === 'subscribe') {
      currentRoom = data.channel;
      if (!rooms.has(currentRoom)) rooms.set(currentRoom, new Set());
      rooms.get(currentRoom).add(ws);
    }

    if (data.type === 'chat') {
      const room = rooms.get(currentRoom);
      if (room) {
        room.forEach(client => {
          if (client.readyState === WebSocket.OPEN) {
            client.send(JSON.stringify({
              type: 'chat',
              userId: ws.userId,
              text: data.text
            }));
          }
        });
      }
    }
  });

  ws.on('close', () => {
    if (currentRoom && rooms.has(currentRoom)) {
      rooms.get(currentRoom).delete(ws);
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Full duplex (both sides can send)
  • Low latency (no HTTP overhead per message)
  • Ideal for high-frequency bidirectional communication

Cons:

  • More complex than SSE
  • Some proxies/firewalls block WebSocket upgrades
  • Stateful connection — harder to load balance
  • Reconnection logic must be implemented manually

Libraries that simplify WebSocket complexity:

// Socket.io handles reconnection, rooms, namespaces, fallbacks
const io = require('socket.io')(server);

io.on('connection', (socket) => {
  socket.on('join-room', (room) => {
    socket.join(room);
  });

  socket.on('send-message', ({ room, text }) => {
    io.to(room).emit('message', { text, from: socket.id });
  });
});
Enter fullscreen mode Exit fullscreen mode

Decision Guide

Polling SSE WebSockets
Direction Client → Server (periodically) Server → Client Both
Protocol HTTP HTTP WS/WSS
Reconnect Manual Automatic Manual
Proxy friendly Yes Usually Sometimes
Complexity Low Medium High
Load balancing Easy Easy Needs sticky sessions

Use polling when:

  • Infrequent updates (> 30 second intervals acceptable)
  • Simple infrastructure, can't add WebSocket support
  • Low traffic, simplicity matters more than efficiency

Use SSE when:

  • Server pushes data to many clients (notifications, feeds, live updates)
  • One-way communication is sufficient
  • You want simplicity with better performance than polling
  • AI text streaming (ChatGPT, Claude streaming responses use SSE)

Use WebSockets when:

  • Bidirectional, low-latency communication (chat, collaborative editing, games)
  • High-frequency data exchange (trading, live sports, multiplayer)
  • Client needs to send data frequently, not just receive

For most web apps, SSE covers 80% of real-time needs at much lower complexity than WebSockets. Reserve WebSockets for genuinely bidirectional, latency-sensitive use cases.

Top comments (0)