Loading content...
In the traditional HTTP request-response model, clients are constantly asking: "Do you have anything new for me?" This polling approach is inefficient—wasting bandwidth when nothing has changed and introducing latency when updates do occur. But what if the server could simply tell the client when something happens?\n\nServer-Sent Events (SSE) represent a paradigm shift in client-server communication. Instead of clients repeatedly asking for updates, servers push data to clients as soon as it becomes available. This one-way streaming model is elegantly simple yet remarkably powerful, forming the backbone of live feeds, real-time dashboards, and notification systems across the modern web.
By the end of this page, you will understand the fundamental architecture of one-way server push, how SSE leverages HTTP to establish persistent streaming connections, the protocol mechanics that enable efficient real-time updates, and why this deceptively simple technology solves complex distributed systems challenges with elegance.
To appreciate the elegance of server push, we must first understand the limitations of the request-response model and the various attempts to work around them.\n\nTraditional Polling (Short Polling)\n\nThe naive approach to real-time updates involves clients periodically requesting data:\n\n``` Client → Server: GET /updates (every 5 seconds) Server → Client: 200 OK, { updates: [] }
Client → Server: GET /updates (5 seconds later) Server → Client: 200 OK, { updates: [] }
// ...99 empty responses later...
Client → Server: GET /updates Server → Client: 200 OK, { updates: [{ message: "Finally!" }] }
Long Polling: A Step Forward\n\nLong polling attempts to address the latency issue by holding the request open until data is available:\n\n``` Client → Server: GET /updates Server: (holds connection open...) Server: (waits for event...) Server → Client: 200 OK, { updates: [{ message: "New data!" }] } Client → Server: GET /updates (immediately reconnects)
Both polling approaches fight against HTTP's fundamental design as a request-response protocol. Server-Sent Events embrace HTTP's strengths (ubiquity, firewall-friendliness, simple caching semantics) while introducing a streaming capability that eliminates the need for repeated requests.
Server-Sent Events provide a standardized, browser-native way to receive a continuous stream of updates from a server over HTTP. Unlike bidirectional WebSockets, SSE focuses on a single, optimized use case: server-to-client streaming.\n\nThe Core Architecture\n\nSSE establishes a unidirectional channel from server to client:
How SSE Works Under the Hood\n\n1. Connection Establishment: The client creates an EventSource object pointing to an SSE endpoint. This initiates a standard HTTP GET request.\n\n2. Content-Type Negotiation: The server responds with Content-Type: text/event-stream, signaling that the response will be a continuous stream rather than a single response body.\n\n3. Keep-Alive: The connection remains open. The server sends events whenever they occur, formatted according to the SSE protocol.\n\n4. Automatic Parsing: The browser's EventSource API automatically parses incoming events and dispatches them to JavaScript handlers.\n\n5. Automatic Reconnection: If the connection drops, EventSource automatically attempts to reconnect, resuming from the last received event.
123456789101112131415161718192021
HTTP/1.1 200 OKContent-Type: text/event-streamCache-Control: no-cacheConnection: keep-alive : This is a comment (lines starting with colon are ignored) data: Simple message with just data field event: userJoineddata: {"userId": "123", "name": "Alice"} id: 12345event: stockUpdate data: {"symbol": "AAPL", "price": 178.50}data: {"change": "+1.2%"}retry: 5000 id: 12346event: stockUpdatedata: {"symbol": "GOOG", "price": 141.20}The SSE protocol is intentionally minimal. Events are plain text, fields are separated by newlines, and messages are separated by blank lines. This simplicity means you can debug SSE with curl, test with any HTTP client, and implement servers in any language with minimal overhead.
The Server-Sent Events protocol defines a simple text-based format for streaming events. Understanding this format is crucial for implementing robust SSE systems.\n\nMessage Fields\n\nEach event can contain the following fields:
| Field | Purpose | Behavior |
|---|---|---|
| data: | Event payload (required) | Multiple data lines are concatenated with newlines. Triggers dispatch when message completes. |
| event: | Event type name | Determines which event listener receives the message. Defaults to 'message' if omitted. |
| id: | Event identifier | Sets lastEventId. Sent to server on reconnection via Last-Event-ID header. |
| retry: | Reconnection delay (ms) | Tells client how long to wait before reconnecting after disconnect. |
| : | Comment | Ignored by client. Useful for keep-alive pings to prevent connection timeout. |
Message Parsing Rules\n\nThe SSE parser follows precise rules that enable reliable streaming:
data: lines within an event are joined with \n characters.12345678910111213
event: notificationid: msg-001data: {data: "type": "alert",data: "title": "System Update",data: "body": "Maintenance scheduled for 3:00 AM UTC",data: "priority": "high"data: } event: heartbeatdata: ping While data fields can span multiple lines for readability in examples, production systems typically send each JSON object on a single data line to simplify parsing and avoid newline handling complexities. The multi-line format is more useful for human-readable messages or when streaming very large objects.
The browser's EventSource API provides a robust, production-ready interface for consuming SSE streams. Understanding its features enables building reliable real-time applications.\n\nBasic Usage
1234567891011121314151617181920212223242526272829303132333435363738394041
// Create connection to SSE endpointconst eventSource = new EventSource('/api/events'); // Handle generic messages (event: field omitted or set to 'message')eventSource.onmessage = (event) => { const data = JSON.parse(event.data); console.log('Message received:', data); console.log('Event ID:', event.lastEventId);}; // Handle specific event typeseventSource.addEventListener('notification', (event) => { const notification = JSON.parse(event.data); showNotification(notification.title, notification.body);}); eventSource.addEventListener('stockUpdate', (event) => { const update = JSON.parse(event.data); updateStockTicker(update.symbol, update.price);}); // Connection lifecycle eventseventSource.onopen = () => { console.log('SSE connection established'); updateConnectionStatus('connected');}; eventSource.onerror = (error) => { if (eventSource.readyState === EventSource.CONNECTING) { console.log('Reconnecting to SSE...'); } else if (eventSource.readyState === EventSource.CLOSED) { console.error('SSE connection failed permanently'); handleConnectionFailure(); }}; // Clean up when no longer neededfunction cleanup() { eventSource.close(); console.log('SSE connection closed');}EventSource Ready States\n\nThe readyState property indicates the connection status:
| State | Value | Description |
|---|---|---|
| CONNECTING | 0 | Connection is being established or reconnection is in progress. |
| OPEN | 1 | Connection is active and receiving events. |
| CLOSED | 2 | Connection is closed and will not reconnect automatically. |
EventSource doesn't support custom headers, which complicates token-based authentication. Common workarounds include: (1) passing tokens as query parameters, (2) using cookies for authentication, or (3) establishing auth via a separate endpoint before SSE connection. For complex auth requirements, consider using a polyfill library like 'event-source-polyfill' that supports custom headers.
Implementing an SSE server requires understanding HTTP streaming, connection management, and event formatting. Let's examine implementations across popular frameworks.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
const express = require('express');const app = express(); // Store connected clientsconst clients = new Map();let clientIdCounter = 0; // SSE endpointapp.get('/api/events', (req, res) => { // Set SSE headers res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering // Prevent request timeout req.setTimeout(0); // Send initial comment to establish connection res.write(': connected\n\n'); // Register client const clientId = ++clientIdCounter; clients.set(clientId, res); console.log(`Client ${clientId} connected. Total: ${clients.size}`); // Handle client disconnect req.on('close', () => { clients.delete(clientId); console.log(`Client ${clientId} disconnected. Total: ${clients.size}`); });}); // Broadcast event to all clientsfunction broadcast(event, data, id = null) { const message = formatSSE(event, data, id); for (const [clientId, res] of clients) { res.write(message); }} // Format SSE messagefunction formatSSE(event, data, id = null) { let message = ''; if (id) message += `id: ${id}\n`; if (event) message += `event: ${event}\n`; message += `data: ${JSON.stringify(data)}\n\n`; return message;} // Send heartbeat every 30 secondssetInterval(() => { for (const [clientId, res] of clients) { res.write(': heartbeat\n\n'); }}, 30000); // Example: broadcast stock updatessetInterval(() => { broadcast('stockUpdate', { symbol: 'AAPL', price: (170 + Math.random() * 10).toFixed(2), timestamp: new Date().toISOString() });}, 5000); app.listen(3000);Many web servers and reverse proxies buffer responses before sending them to clients. This defeats SSE's real-time nature. Always disable buffering with appropriate headers (X-Accel-Buffering: no for Nginx) and ensure your application framework flushes responses immediately.
At first glance, SSE's one-way nature might seem like a limitation. After all, WebSockets offer bidirectional communication. However, for many use cases, the one-way model is not just sufficient—it's preferable.\n\nThe Hybrid Architecture Pattern\n\nIn practice, most real-time applications combine SSE for server push with regular HTTP for client-to-server communication:
This pattern offers significant advantages over pure WebSocket implementations:
Choose the simplest technology that solves your problem. If your real-time needs are server-to-client only, SSE's simplicity, native browser support, and HTTP compatibility make it the right choice. Reserve WebSockets for truly bidirectional scenarios like collaborative editing or multiplayer games.
We've established the foundational concepts of Server-Sent Events and the one-way push model. Let's consolidate the key takeaways:
What's Next:\n\nNow that we understand the fundamentals of SSE and one-way server push, we'll examine how SSE compares to WebSockets in detail. Understanding the trade-offs between these technologies is crucial for making the right architectural decisions.
You now understand the architecture and mechanics of one-way server push with SSE. You can implement basic SSE servers and clients, and you understand when this pattern is preferable to bidirectional alternatives. Next, we'll explore SSE vs WebSocket trade-offs in detail.