Building real-time features on the web raises an early decision: WebSocket or Server-Sent Events? Both push live data to the browser, but their architectures differ. This article compares them on concrete criteria and explains which to pick in which scenario.
Core Differences
SSE — The Simplest Start
// Server — Express
app.get('/events', (req, res) => {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
const send = (data, event = null) => {
if (event) res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
send({ status: 'connected' }, 'init');
const interval = setInterval(() => {
send({ time: new Date().toISOString(), cpu: os.loadavg()[0] });
}, 1000);
req.on('close', () => clearInterval(interval));
});
// Client
const es = new EventSource('/events');
es.addEventListener('init', e => console.log('Connected', JSON.parse(e.data)));
es.onmessage = e => {
const data = JSON.parse(e.data);
document.getElementById('cpu').textContent = data.cpu;
};
es.onerror = () => console.warn('SSE error, auto-reconnecting...');
// The browser reconnects automatically — no manual retry code needed
WebSocket — Full Duplex
// Server — ws library
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws, req) => {
console.log('New client:', req.socket.remoteAddress);
ws.send(JSON.stringify({ type: 'welcome' }));
ws.on('message', data => {
const msg = JSON.parse(data);
if (msg.type === 'ping') ws.send(JSON.stringify({ type: 'pong' }));
});
ws.on('close', () => console.log('Disconnected'));
});
// Broadcast
function broadcast(data) {
wss.clients.forEach(c => {
if (c.readyState === c.OPEN) c.send(JSON.stringify(data));
});
}
// Client
const ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => ws.send(JSON.stringify({ type: 'ping' }));
ws.onmessage = e => console.log(JSON.parse(e.data));
ws.onclose = () => setTimeout(() => reconnect(), 3000);
// You have to write the reconnection logic yourself
Socket.io — The Abstraction
Brings WebSocket reconnection, long-polling fallback, rooms and namespaces out of the box. It is a heavy abstraction though — start with raw ws if you only need one capability.
const io = require('socket.io')(server);
io.on('connection', socket => {
socket.join('room:general');
socket.on('chat:message', msg => {
io.to('room:general').emit('chat:message', {
from: socket.id, text: msg, at: Date.now()
});
});
});
When to Pick Each
Pick SSE when:
- One-way server notifications: notifications, live score, stock price, ChatGPT-style streaming
- Simpler codebase: native EventSource plus auto-reconnect
- HTTP/2: multiple streams over the same connection
- Proxy-friendly: plain HTTP
- LLM streaming (OpenAI, Anthropic APIs)
Pick WebSocket when:
- Two-way communication: chat, collaborative editing, multiplayer games
- Low latency required: trading, gaming
- Binary data: file transfer, streaming
- High message frequency: SSE's HTTP header overhead adds up
Scaling
Both protocols hold stateful connections, so your load balancer must support sticky sessions. When you scale across multiple Node instances, use Redis Pub/Sub or NATS to broadcast messages between them.
// Socket.io + Redis adapter
const { createAdapter } = require('@socket.io/redis-adapter');
const pubClient = new Redis();
const subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));
// Every Node instance now sees every message
Nginx Proxy Configuration
# WebSocket needs the upgrade header forwarded
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s; # long connections
}
# SSE needs buffering disabled
location /events {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
}
Authentication
The WebSocket handshake is HTTP, so you can auth via cookie or header. But once the connection is open, you cannot re-authenticate (token expiry is awkward). Long-lived connections need a token-rotation plan. SSE is simpler — each reconnect re-authenticates naturally.
Conclusion
Starting fresh and not sure you really need two-way? Start with SSE. It is simpler, proxy-friendly and reconnects out of the box. For chat, games and collaborative editors, WebSocket is the right call. Every full-stack engineer should be comfortable with both — and KEYDAL can help design the right real-time architecture for your product.
Build real-time features with WebSocket, SSE or a hybrid architecture Contact us