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 

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 } } } 
// 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' }); } }); 

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 
// 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(); }); }); 

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!' })); 
// 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); } }); }); 

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 }); }); }); 

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.