Real-Time Streaming with Server-Sent Events (SSE)
Introduction to Unidirectional Real-Time Streaming
Developing modern, interactive web applications requires mechanisms for pushing real-time updates from the server to the client. Historically, engineers relied on polling or long-polling, both of which introduce high HTTP overhead and latency. For bidirectional communication, WebSockets became the industry standard; however, WebSockets operate over a custom stateful protocol (ws://) that bypasses traditional HTTP infrastructure, requiring specialized load balancers, connection managers, and firewall rules.
For applications requiring strictly unidirectional data flows—such as live news feeds, financial tick updates, system monitoring dashboards, or generative AI streaming responses—Server-Sent Events (SSE) offer an elegant, lightweight alternative. SSE operates natively over standard HTTP/1.1 and HTTP/2 transport layers, leveraging existing web infrastructures while providing a robust, simple streaming API.
The Mechanics of text/event-stream
The core mechanism of Server-Sent Events relies on a persistent, long-lived HTTP connection. The client initiates a standard HTTP request to the server, specifying an Accept header of text/event-stream. The server responds with an HTTP status code 200, setting the Content-Type header to text/event-stream and the Connection header to keep-alive.
Crucially, the response body is kept open indefinitely. Instead of terminating the stream, the server writes structured data chunks sequentially over the open TCP socket. This format allows the browser to parse each incoming message stream incrementally, emitting events to the application layer without closing the connection or negotiating new handshakes.
The SSE text protocol defines a set of standard keys that control message streaming and client state updates:
- event: A custom event name allowing the client to register specific event listeners.
- data: The string payload containing the message content, typically formatted as JSON.
- id: A unique identifier that the client uses to resume the stream from the last received event.
- retry: The reconnection timeout in milliseconds, instructing the client when to reconnect after a disconnect.
The SSE protocol defines a highly specific, line-based text format for transmitting messages. Every block of data is separated by two consecutive newline characters (\n\n). Within a block, lines are structured as key-value pairs separated by a colon.
The protocol supports four standard keys: event, data, id, and retry. The event field specifies a custom event name, allowing the client to listen for distinct event types. The data field holds the string payload of the message.
The id field assigns a unique identifier to the message, which the browser uses to track stream progress. Lastly, the retry field specifies the reconnection timeout in milliseconds, instructing the browser how long to wait before attempting to reconnect if the connection drops.
# Example of raw SSE stream format transmitted by the server
event: stock-update
data: {"symbol": "AAPL", "price": 175.50}
id: 1001
event: stock-update
data: {"symbol": "GOOG", "price": 142.20}
id: 1002
Built-In Resilience and Event Reconnection
A primary architectural benefit of SSE over WebSockets is its built-in resilience. The browser's native EventSource API manages the connection lifecycle automatically. If the network drops or the server terminates the connection, the browser will automatically attempt to re-establish the connection in the background using exponential backoff.
During this reconnection request, the browser automatically appends a Last-Event-ID HTTP header containing the last successfully processed message ID. This allows the server to identify exactly where the client's stream was interrupted and replay any missed messages, ensuring zero data loss during transient network disruptions.
HTTP/2 Multiplexing and Connection Limitations
When implementing SSE over older HTTP/1.1 connections, developers must be mindful of browser connection limits. Standard browsers restrict the number of concurrent connections to a single domain to six. Because an SSE connection is long-lived, opening multiple SSE streams across different browser tabs can quickly exhaust this limit, blocking all subsequent HTTP requests to that domain.
However, this limitation disappears entirely when operating over HTTP/2 or HTTP/3. HTTP/2 utilizes multiplexing, allowing hundreds of concurrent requests and active streams to share a single TCP connection. Consequently, deploying SSE in production requires ensuring that the hosting infrastructure supports HTTP/2 or HTTP/3 natively.
Client and Server Implementation Patterns
Compared to WebSockets, SSE is exceptionally developer-friendly and integrates seamlessly with standard web security practices. Because SSE uses standard HTTP requests, it inherits existing authentication headers, cookie handling, Cross-Origin Resource Sharing (CORS) rules, and SSL/TLS encryption configurations. Developers do not need to configure specialized WebSocket gateways or handle complex protocol switching.
However, because the connection is unidirectional, clients cannot send data back over the same stream. If bidirectional interaction is required, clients must send separate HTTP POST requests, creating a hybrid architecture that balances the simplicity of HTTP with the responsiveness of real-time streaming.
// Client-side EventSource implementation with custom event listeners
const eventSource = new EventSource('/api/stream');
eventSource.onmessage = (event) => {
console.log('Generic message received:', event.data);
};
eventSource.addEventListener('stock-update', (event) => {
const stock = JSON.parse(event.data);
console.log(`Stock ${stock.symbol} updated to ${stock.price}`);
});
eventSource.onerror = (error) => {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Stream connection closed by server');
} else {
console.error('Stream error occurred, reconnecting...');
}
};
On the server side, keeping connections open for long periods requires non-blocking asynchronous runtimes. In traditional thread-per-request servers, maintaining thousands of active SSE connections would quickly exhaust the server's thread pool, leading to complete service failure. Modern runtimes like Node.js, Go, or Rust handle persistent connections with minimal overhead by utilizing event-driven, non-blocking I/O multiplexing.
In serverless environments, however, maintaining long-lived connections is cost-prohibitive due to execution duration limits. In these cases, offloading the connection state to an edge proxy or a dedicated publish-subscribe broker is the recommended architectural pattern.
Server-Sent Events Optimization at the Edge with Bramsley
"Global real-time streaming requires decoupling long-lived connection states from serverless function durations to avoid extreme cost spikes."
Our real-time systems architects design and optimize low-latency event distribution networks:
- Multiplexed Connection Pooling: Leverage HTTP/2 and HTTP/3 multiplexing at edge routers to scale millions of concurrent SSE connections.
- Edge-Cached Event Hubs: Offload persistent connection states to edge-native workers, streaming push notifications directly from the edge.
- Optimized JSON Payloads: Compress and serialize real-time payloads dynamically to reduce egress costs and network latency.
Reach out to Bramsley Digital Studio to secure your real-time data pipelines and reduce streaming overhead. Get started with our real-time streaming engineers.