A photo of Maximilian Schwarzmüller

Maximilian Schwarzmüller

Server-sent Events (SSE): The Champion Nobody Knows

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:

  1. HTTP-Based: SSE operates over standard HTTP/HTTPS, making it generally easier to integrate with existing infrastructure (proxies, firewalls) compared to WebSockets.
  2. 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).
  3. 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.
  4. Text-Based: The data format is simple, human-readable text (text/event-stream).
  5. 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:

  1. Accept a client connection.
  2. Send specific HTTP headers to indicate an SSE stream.
  3. Keep the connection open.
  4. Send data formatted according to the text/event-stream specification whenever new information is ready.

Required Response Headers:

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:

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:

This core difference leads to several practical implications:

  1. 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.
  2. 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:// or wss://), initiated via an HTTP “Upgrade” request. While widely supported now, this upgrade mechanism can sometimes be blocked by older or stricter intermediaries.
  3. Reconnection: The browser’s EventSource API for SSE handles reconnections automatically if the connection drops. It even uses the id field (if provided by the server) to tell the server the last message it received via the Last-Event-ID header. With WebSockets, you have to manually implement all reconnection logic (detecting drops, backoff strategies, resynchronizing state) in your JavaScript code.
  4. 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:

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:

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.

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.