WebSockets & Server-Sent Events (SSE): Real-Time Bi-Directional vs. Unidirectional Streaming
Architecting real-time applications: full-duplex TCP WebSockets vs. lightweight HTTP/2 SSE streaming for LLM tokens, live notifications, and market data feeds.
Building responsive real-time web applications requires replacing legacy HTTP short-polling techniques with persistent, event-driven streaming connections. Polling repeatedly floods web servers with redundant HTTP request headers, exhausts TCP port pools, and introduces latency gaps. For persistent streaming, modern architectures rely on two primary technologies: WebSockets (RFC 6455) for full-duplex bi-directional communication, and Server-Sent Events (SSE) for lightweight unidirectional server-to-client event streaming. Choosing between them requires evaluating communication topology, proxy/firewall traversal, connection state scaling, and protocol overhead.
⚙️ The WebSockets Over-Engineering Trap
Engineering teams frequently reflexively default to WebSockets whenever real-time functionality is mentioned. However, over 80% of real-time application requirements—such as live LLM token streaming, notifications, stock price tickers, dashboard telemetry, and order status updates—are strictly unidirectional (server-to-client). In these scenarios, Server-Sent Events over HTTP/2 are superior: SSE provides native browser auto-reconnection, event ID resumption, transparent corporate proxy traversal, and zero custom framing overhead.
1. Protocol Comparison: WebSockets vs. Server-Sent Events (SSE)
Evaluating the architectural boundaries between full-duplex TCP framing and unidirectional HTTP/2 streaming:
| Architectural Vector | WebSockets (RFC 6455) | Server-Sent Events (SSE / EventSource) |
|---|---|---|
| Communication Direction | Full-Duplex (Bi-directional simultaneous) | Unidirectional (Server-to-client only) |
| Underlying Transport | Independent TCP connection (Upgraded from HTTP/1.1) | Standard HTTP/1.1 or HTTP/2 persistent stream |
| HTTP/2 Multiplexing | No (Requires distinct TCP socket per connection) | Yes (Streams share single multiplexed HTTP/2 connection) |
| Data Serialization | Binary (ArrayBuffer, Blob) and UTF-8 Text | UTF-8 Text only (text/event-stream) |
| Reconnection & Resumption | Manual client implementation required | Native browser auto-reconnect with Last-Event-ID |
| Firewall & Proxy Traversal | May be blocked by enterprise deep-packet inspection | 100% transparent (Standard HTTPS port 443 traffic) |
| Mobile Battery & Radio Impact | Requires frequent application ping-pong heartbeats | Leverages standard HTTP connection keep-alive |
2. Server-Sent Events: Simplicity, HTTP/2 Multiplexing & EventSource
Server-Sent Events utilize the browser's native EventSource API. A client issues a standard HTTP GET request with Accept: text/event-stream, and the server keeps the connection open indefinitely, streaming text chunks formatted with data:, event:, and id: fields.
- HTTP/2 Zero-Cost Connections: Under HTTP/1.1, browsers enforce a strict limit of 6 simultaneous persistent connections per domain. Under HTTP/2, multiple SSE streams multiplex cleanly over a single underlying TCP connection, eliminating head-of-line blocking and socket exhaustion.
- Native Resilience with
Last-Event-ID: If a network blip or cellular handover disconnects an SSE stream, the browser automatically reconnects after a configurable retry interval. The browser automatically sends theLast-Event-IDheader containing the last received message identifier. The server inspects this header, replays any missed messages from a buffer or Redis Stream, and resumes streaming with zero data loss.
3. WebSockets: Low-Latency Full-Duplex Framing for Interactive Workloads
When an application requires continuous, high-frequency, two-way interaction—such as collaborative document editing (CRDTs), multiplayer gaming, live chat room messaging, or trading order placement—WebSockets are indispensable.
- The HTTP Upgrade Handshake: The client initiates an HTTP/1.1
GETrequest with headersUpgrade: websocketandConnection: Upgrade, alongside a cryptographicSec-WebSocket-Key. The server responds withHTTP/1.1 101 Switching Protocols, converting the TCP socket into a bidirectional WebSocket frame pipe. - Minimal Framing Overhead: Once established, WebSocket frames carry merely 2 to 10 bytes of framing overhead per packet, drastically lower than repeating HTTP headers.
- Ping-Pong Heartbeats & Ghost Connections: Network middleboxes (NAT firewalls, load balancers) silently drop idle TCP sockets without sending a FIN/RST packet. WebSocket servers must implement strict ping-pong heartbeat timers (e.g., every 30 seconds) to terminate ghost connections and free server memory.
4. Scaling Real-Time Connections Across Clustered Nodes
Because real-time connections are stateful and persistent, scaling horizontally across multi-node Kubernetes clusters requires careful network design:
- Redis Pub/Sub Backplane: When Client A connects to Node 1 and Client B connects to Node 2, Node 1 cannot send messages directly to Client B. Deploying a Redis Pub/Sub or Kafka backplane ensures that when an event occurs, it is broadcast to all cluster nodes, which dispatch it to their locally attached client sockets.
- Sticky Sessions vs. Stateless Handshakes: Layer 7 load balancers (Nginx, AWS ALB, Traefik) must configure session stickiness (or cookie routing) during WebSocket upgrades to ensure polling handshakes land on the identical server instance.
5. Production Implementation: Resilient SSE Server & Client
The following production TypeScript blueprint demonstrates an enterprise Server-Sent Events endpoint in Express/Node.js with support for Last-Event-ID replay and client reconnection handling:
💻 Production Server-Sent Events (SSE) Streaming Blueprint
import express, { Request, Response } from 'express';
const app = express();
interface StreamEvent {
id: string;
event: string;
data: any;
}
// In-memory sliding buffer for message replay during disconnects (use Redis in cluster)
const eventHistory: StreamEvent[] = [];
export function sseHandler(req: Request, res: Response) {
// 1. Mandatory SSE Response Headers
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' // Disables Nginx reverse proxy buffering
});
// Keep-alive heartbeat every 20 seconds to prevent proxy socket timeout
const keepAliveTimer = setInterval(() => {
res.write(': keep-alive ping
');
}, 20000);
// 2. Handle Reconnection Resumption with Last-Event-ID
const lastEventId = req.headers['last-event-id'] as string;
if (lastEventId) {
const replayIndex = eventHistory.findIndex(e => e.id === lastEventId);
if (replayIndex !== -1) {
// Replay all missed events since last received ID
for (let i = replayIndex + 1; i < eventHistory.length; i++) {
sendSseMessage(res, eventHistory[i]);
}
}
}
// 3. Dispatch Live Application Event
const onNewEvent = (event: StreamEvent) => {
sendSseMessage(res, event);
};
eventBus.on('telemetry_update', onNewEvent);
// 4. Clean up resources upon client disconnect
req.on('close', () => {
clearInterval(keepAliveTimer);
eventBus.off('telemetry_update', onNewEvent);
res.end();
});
}
function sendSseMessage(res: Response, { id, event, data }: StreamEvent) {
res.write(`id: ${id}
`);
res.write(`event: ${event}
`);
res.write(`data: ${JSON.stringify(data)}
`);
}
⚡ Moemecto Distributed Systems Advisory
Design fault-tolerant distributed systems, optimize database query execution plans, and architect zero-trust microservice meshes with Moemecto. Schedule an Architecture Review →
Architecting Your Next Distributed Platform?
Collaborate with our software architects on microservices decomposition, IoT firmware, and database sharding.