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

Related guides: PostgreSQL optimization · Advanced Git commands · What is Redis · Deploying with Docker · Docker Compose guide

WebSocketSSE
DirectionFull-duplex (two-way)Server-to-client only
Protocolws:// / wss:// (HTTP upgrade)HTTP/1.1 or HTTP/2
ReconnectionManual / Socket.ioAutomatic (EventSource)
BinaryYesNo (text only)
Proxy compatibilityRequires upgrade headerStandard HTTP
Browser support~98%~95% (no IE)
Header authOnce, at connection timeOn every reconnect

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;
}
Warning
Cloudflare's free plan does not drop WebSockets every 100 seconds, but some enterprise proxies idle-timeout after 60. Send a heartbeat ping every 30 seconds to be safe.

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.

Modern Software Development and DevOps Practices

Professional software development rests on three pillars: version control (Git + GitHub/GitLab pull request flow, mandatory code review), CI/CD pipeline (automated test + lint + build + deploy), and observability (Sentry/Datadog/Grafana for logs, metrics, traces). The test pyramid (unit > integration > e2e) ensures code quality, microservice architecture uses Docker containers with Kubernetes orchestration, and REST or GraphQL APIs follow OpenAPI/GraphQL Schema contracts. Across the SDLC (requirements → design → implementation → test → deploy → maintenance), Agile/Scrum sprints last 1-2 weeks while DevOps teams practice continuous delivery.

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.

Real-time feature development

Build real-time features with WebSocket, SSE or a hybrid architecture Contact us

WhatsApp