
Server-sent Events (SSE): The Champion Nobody Knows
Published
Need to sendNeed to send data from a server to the client (e.g., browser) without the client explicitly asking for it (i.e., without sending a request from the client to the server)?
You need to use Websockets, right?
No!
You actually don’t - there is a solution that’s often easier to implement. Yet no one knows about it!
Enter: Server-Sent Events (SSE) - a stable web standard available for well over a decade.
What are Server-Sent Events (SSE)?
Server-Sent Events (SSE) is a standard technology enabling a web server to push data updates to a client (typically a web browser) over a single, long-lived HTTP connection. Once the initial connection is established by the client, the server can send data unidirectionally (server-to-client) whenever new information is available.
That’s therefore the opposite direction you might be used to. Instead of client request => response, it’s now an “extra response” sent by the server at some point in the future.
Think of it like a subscription: the client subscribes to an event stream from the server, and the server sends messages down that stream as they occur.
Key characteristics:
- HTTP-Based: SSE operates over standard HTTP/HTTPS, making it generally easier to integrate with existing infrastructure (proxies, firewalls) compared to WebSockets.
- Unidirectional: Communication flows strictly from the server to the client. The client cannot send data back to the server over the same SSE connection (it would need a separate HTTP request for that).
- Automatic Reconnection: Browsers implementing the
EventSource
API (the client-side interface for SSE) automatically attempt to reconnect if the connection is dropped. This is a significant advantage over manually implementing reconnection logic. - Text-Based: The data format is simple, human-readable text (
text/event-stream
). - Event Types: Allows sending different types of events, enabling clients to handle various updates differently.
SSE is ideal for scenarios like live news feeds, stock tickers, notification systems, real-time dashboards, or any situation where the server needs to inform the client about changes without the client constantly asking (polling).
SSE Usage - Server-side
The server’s responsibility is to:
- Accept a client connection.
- Send specific HTTP headers to indicate an SSE stream.
- Keep the connection open.
- Send data formatted according to the
text/event-stream
specification whenever new information is ready.
Required Response Headers:
Content-Type: text/event-stream
: Tells the client this is an SSE stream.Cache-Control: no-cache
: Prevents intermediate proxies from caching the response.Connection: keep-alive
: Instructs the network stack to keep the connection open (often handled automatically by modern HTTP servers).
Event Stream Format:
Now, to be honest, those server-sent events look a bit weird.
Because those messages are sent as plain text, UTF-8 encoded, and separated by two newline characters (\n\n
). Each message can consist of one or more fields, where each field is a key-value pair followed by a single newline (\n
). Common fields include:
data:
: The actual payload of the message. You can have multipledata:
lines for multi-line messages.event:
: An optional name for the event type. If omitted, the message triggers the genericonmessage
handler on the client. Providing a specific event name allows the client to listen for that event (and execute different code for different events that may be sent by different servers)id:
: An optional unique ID for the event. If the connection drops, the browser will send the last receivedid
in aLast-Event-ID
HTTP header upon reconnection, allowing the server to potentially resend missed events.retry:
: An optional integer specifying the reconnection time in milliseconds. The browser will wait this long before attempting to reconnect if the connection is lost.
Here’s an example for a Node / Express server sending such a SSE:
import express from 'express';
import cors from 'cors';
const app = express();
const port = 3000;
app.use(cors()); // Allow requests from frontend domain that differs from server's domain
// In-memory list of connected clients
let clients = [];
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
const clientId = Date.now(); // dummy client id
const newClient = {
id: clientId,
res,
};
clients.push(newClient);
console.log(`Client ${clientId} connected. Total clients: ${clients.length}`);
res.write(`data: You are connected with ID: ${clientId}\n\n`);
// Handle client disconnect
req.on('close', () => {
console.log(`Client ${clientId} disconnected.`);
clients = clients.filter((client) => client.id !== clientId);
});
});
function sendEventToAll(data, eventName = null) {
console.log(
`Sending event: ${eventName || 'message'}, data: ${JSON.stringify(data)}`
);
clients.forEach((client) => {
let message = '';
if (eventName) {
message += `event: ${eventName}\n`;
}
message += `data: ${JSON.stringify(data)}\n\n`; // Always end with \n\n!
client.res.write(message);
});
}
app.listen(port, () => {
console.log(`SSE server listening at http://localhost:${port}`);
});
SSE Usage - Client-side
On the client-side (in the browser), you use the EventSource
interface for connecting & listening to events:
const messageList = document.getElementById('message-list');
const timeDisplay = document.getElementById('time-display');
const statusDisplay = document.getElementById('connection-status');
const evtSource = new EventSource('http://localhost:3000/events');
statusDisplay.textContent = 'Connecting...';
// Handle Generic Messages (No 'event' field)
evtSource.onmessage = function (event) {
console.log('Generic message received:', event);
const data = JSON.parse(event.data); // Assuming server sends JSON
const newItem = document.createElement('li');
newItem.textContent = `Message: ${data.message || event.data}`;
messageList.appendChild(newItem);
};
// Handled Named Events (e.g., listen to events named "special")
evtSource.addEventListener('special', function (event) {
console.log('Special event update received:', event);
const data = JSON.parse(event.data);
const newItem = document.createElement('li');
newItem.textContent = `Special Event Message: ${data.message || event.data}`;
messageList.appendChild(newItem);
});
// Handle Errors
evtSource.onerror = function (err) {
console.error('EventSource failed:', err);
statusDisplay.textContent =
'Connection Error or Closed. Trying to reconnect...';
};
evtSource.onopen = function () {
console.log('Connection to server opened.');
statusDisplay.textContent = 'Connected';
};
// You can also explicitly close the connection
// You might have a button or condition to close the connection
// const closeButton = document.getElementById('close-btn');
// closeButton.onclick = function() {
// console.log('Closing connection.');
// evtSource.close();
// statusDisplay.textContent = 'Connection closed by client.';
// };
Server-sent Events vs Websockets
Of course, you might wonder: “What’s the purpose of SSEs? Don’t we already have WebSockets?”.
And you’re right - WebSockets kind of solve a similar problem. But then again, they don’t. The fundamental difference lies in directionality:
- Server-Sent Events (SSE): Communication is strictly unidirectional. Only the server can send messages to the client after the initial connection is established. The client cannot send data back over the same connection.
- WebSockets (WS): Communication is bidirectional. Both the client and the server can send messages to each other at any time over the same persistent connection.
This core difference leads to several practical implications:
- Complexity: Because SSE is unidirectional, it’s often much simpler to implement on both the server and client if all you need is server-to-client push. You don’t need logic to handle incoming messages from the client on that channel. WebSockets inherently require handling for two-way communication, increasing complexity.
- Protocol & Infrastructure: SSE works over standard HTTP/HTTPS. This means it usually plays nicely with existing web infrastructure like firewalls and proxies. WebSockets use a different protocol (
ws://
orwss://
), initiated via an HTTP “Upgrade” request. While widely supported now, this upgrade mechanism can sometimes be blocked by older or stricter intermediaries. - Reconnection: The browser’s
EventSource
API for SSE handles reconnections automatically if the connection drops. It even uses theid
field (if provided by the server) to tell the server the last message it received via theLast-Event-ID
header. With WebSockets, you have to manually implement all reconnection logic (detecting drops, backoff strategies, resynchronizing state) in your JavaScript code. - Data Format: SSE is designed for sending text events (specifically UTF-8 encoded text formatted as
text/event-stream
). WebSockets have native support for both text (UTF-8) and binary data messages.
When to choose which:
- Use SSE if your primary need is for the server to push updates, notifications, or data streams to the client without needing immediate responses or commands from the client over the same channel (e.g., live news feeds, stock tickers, activity logs, status updates).
- Use WebSockets if you need true real-time, two-way interaction between the client and server (e.g., chat applications, multi-user collaborative editing, real-time multiplayer games).
Server-Sent Events vs Streams
It’s important to clarify what “Streams” means here. SSE is fundamentally a streaming technology – it streams data over a persistent connection. However, it’s usually compared to lower-level streaming concepts or APIs:
- SSE vs Browser Streams API (
ReadableStream
): The Streams API provides low-level primitives in the browser for handling streams of data (often used withWorkspace
). You could technically useWorkspace
to get the response stream from an SSE endpoint and parse thetext/event-stream
format manually using aReadableStream
. However, this is significantly more complex than using the dedicatedEventSource
API. EventSource
API Advantages: TheEventSource
API is the high-level, purpose-built interface for SSE. It handles:- Parsing the specific
text/event-stream
message format. - Dispatching messages based on the
event:
field. - Automatic reconnection logic (using the
retry:
field or defaults). - Tracking the
id:
field and sendingLast-Event-ID
on reconnect.
- Parsing the specific
- SSE vs Server-Side Streams: On the server (like in Node.js), you typically write SSE data to a response object that often behaves like a writable stream. So, SSE uses streaming concepts on the backend.
In essence, SSE is a specific application-level protocol built on top of underlying HTTP streaming mechanisms, and EventSource
is the convenient browser API for it. While you could use lower-level stream APIs, EventSource
abstracts away the complexities for the specific use case of consuming server-sent events.
Server-Sent Events vs Regular Requests
This comparison highlights the difference between proactive server pushes and reactive client pulls.
-
Regular HTTP Requests:
- Lifecycle: These are typically short-lived. The client sends a request, the server processes it and sends a response, and the connection closes (or is returned to a pool).
- Initiation: Always initiated by the client when it needs something.
- Pattern: Follows the classic request-response model.
- Real-time Updates: To get updates, the client must repeatedly ask the server (polling) or use techniques like long-polling (where the server holds the request open), both of which introduce latency and overhead compared to SSE.
- Use Case: Fetching web pages, API data, images, submitting forms – essentially, any time the client needs to explicitly ask for a resource or send data.
-
Server-Sent Events:
- Lifecycle: Establishes a single, long-lived connection that remains open.
- Initiation: The initial connection is made by the client, but subsequent data flow is initiated by the server whenever it has new information to send.
- Pattern: A publish-subscribe model where the client subscribes to an event stream from the server.
- Efficiency: For frequent server-to-client updates, SSE is far more efficient than polling, as it avoids the overhead of establishing new connections and sending redundant headers for each update.
- Use Case: Situations where the server needs to inform clients about events asynchronously, such as notifications, live data feeds, or status changes.
Choose Regular Requests when the client dictates when it needs data. Choose SSE when the server needs to dictate when the client receives new data.