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.