Server-Sent Events (SSE): A Conceptual Deep Dive

We’re rapidly heading toward an event-driven world of data streams and APIs. Server-Sent Events fills a specific niche in this new world: an open, lightweight, subscribe-only protocol for event-driven data streams. This article explores how SSE came to be, how it works under the hood, and why it’s rapidly being adopted by developers.

Copy link to clipboardA bit of background

SSE is based on something called Server-Sent DOM Events, which was first implemented in Opera 9. The idea is simple: a browser can subscribe to a stream of events generated by a server, receiving updates whenever a new event occurs. This led to the birth of the popular EventSource interface, which accepts an HTTP stream connection and keeps the connection open while retrieving available data from it. The connection is kept open until closed by calling EventSource.close().

Copy link to clipboardWhat is Server-Sent Events (SSE)?

SSE is a standard describing how servers can initiate data transmission towards clients once an initial client connection has been established. It provides a memory-efficient implementation of XHR streaming. Unlike a raw XHR connection, which buffers the full received response until the connection is dropped, an SSE connection can discard processed messages without accumulating all of them in memory.

SSE is designed to use the JavaScript EventSource API in order to subscribe to a stream of data in any popular browser. Through this interface a client requests a particular URL in order to receive an event stream. SSE is commonly used to send message updates or continuous data streams to a browser client.

In a nutshell, a server-sent event is when updates are pushed (rather than pulled, or requested) from a server to a browser.

Copy link to clipboardHow does SSE work?

A connection over SSE typically begins with client-initiated communication between client and server. The client creates a new JavaScript EventSource object, passing the URL of an endpoint to the server over a regular HTTP request. The client expects a response with a stream of event messages over time.

The server leaves the HTTP response open until it has no more events to send, it decides that the connection has been open long enough and can be considered stale, or until the client explicitly closes the initial request.

sse-diagram

How SSE works from connection request to close

Here’s a quick example of opening a stream over SSE:

var source = new EventSource('URL_TO_EVENT_STREAM');
source.onopen = function() {
   console.log('connection to stream has been opened');
};
source.onerror = function (error) {
  console.log('An error has occurred while receiving stream', error);
};
source.onmessage = function (stream) {
  console.log('received stream', stream);
};

Copy link to clipboardWhy would you use SSE?

Ideally, when requesting data from a server, a simple XMLHttpRequest will do. However, there are scenarios where you’d want to keep the connection open using XHR streaming. But this brings its own set of overheads, including handling parsing logic and connection management.

This is where SSE comes in. SSE provides a layer of connection management and parsing logic, allowing us to easily keep the connection open while a server pushes new events to the client as they become available.

Copy link to clipboardWhen would you use SSE?

The nature of realtime messaging and streaming data mean different protocols serve different purposes better than others. For multiplexed, bidirectional streaming, WebSockets is perfect. For IoT devices with limited battery life, MQTT is more suitable. But sometimes these are overkill.

SSE is perfect for scenarios such as:

  • When an efficient unidirectional communication protocol is needed that won’t add unnecessary server load (which is what happens with long polling)

  • When you need a protocol with a predefined standard for handling errors

  • When you want to use HTTP-based methods for realtime data streaming

  • When you need a unidirectional protocol with better latency for users than other HTTP-based ways of streaming data

Here’s a few examples where SSE is already in use:

  • Subscribing to a feed of cryptocurrency or stock prices

  • Subscribing to a Twitter feed

  • Receiving live sports scores

  • News updates or alerts

Copy link to clipboardThings to consider with SSE 

As with all things, SSE has its own challenges.

The major limitation of SSE is that it’s unidirectional so there’s no way to pass information to a server from a client. The only way to pass additional data is at the time of connection, which many developers choose to do with query strings. For example, if a stream URL is http://example.com/sse, developers can add a query string to pass information such as a user id, e.g. http://example.com/sse?userid=891.

This unidirectionality causes an additional problem: when a client loses a connection there’s no reliable way to let the server know as there’s no way to perform client-to-server heartbeats. As SSE is based on TCP/IP, there is a mechanism that alerts a server when a client loses a connection. But it doesn’t always work well so the server doesn’t always immediately realise a connection is lost. However, this is a minor limitation to SSE.

Copy link to clipboardSSE support

SSE is widely supported across popular browsers – which in turn means it’s supported by a range of mobile and embedded IoT devices. That said, before attempting to implement SSE it’s worth checking support coverage for browsers your application or service supports.

sse-browser-support

Browsers that support SSE as of June 2019

It’s also worth checking EventSource support. See the example below, which uses polyfill:

if ('EventSource' in window) {
  // use polyfills
}
var source = new EventSource('URL');

A great example of polyfill for EventSource is Yaffle’s EventSource polyfill.

Copy link to clipboardUsing SSE at scale

Since SSE is based on the HTTP protocol, scaling can be achieved with means such as load-balancing. However, you’d need to ensure some sort of shared resource behind the servers so they’re all kept in sync with new event updates.

Many choose to handle this with the publish/subscribe architecture design pattern. With pub/sub, events aren’t sent directly to clients but instead are sent to the broker. The broker then sends the message to all subscribers (which may include the original sender). To host your own pub/sub mechanism, you could consider using Redis.

If you’d rather offload this complexity of scaling and focus on your core product engineering, you can rely on realtime messaging platforms that offer event-stream and SSE endpoints. Check out this detailed explanation of how to implement SSE using a realtime messaging platform that supports the protocol.

Copy link to clipboardHow to use SSE in the best way possible

While SSE is easy to implement, often times developers tend to get stuck while sending data. SSE represents data in an actual stream, so should we need to send JSON with SSE, it looks something like this:

res.write('data: {\n');
res.write('data: "foo": "bar",\n');
res.write('data: "baz", 555\n');
res.write('data: }\n\n');

For developers used to writing pure JavaScript, when implementing SSE they might end up with invalid JSON while parsing on the client side. Using a library that abstracts this with plain JavaScript can help. For example, using the express-sse library with Express as your web framework. Here’s an example of SSE from the server using Node’s Express:

app.get('/stream', (req, res)=>{
    res.status(200).set({
      "connection": "keep-alive",
      "content-type": "text/event-stream"
    });
    res.write(`data: Hello there \n\n`);
});

Looking at the code above, we notice three things:

  • Text/event-stream content type header. This is what browsers look for to confirm an event stream.

  • Keep-alive header. This tells the browser not to close the connection.

  • The double new-line characters at the end of data. This indicates the end of the message.

However, looking at the example above, it’s only useful when sending one event or using the setInterval function to continuously check the database for new connections.

If we were to use the express-sse library, for example, it exposes a method where we can emit to the stream from anywhere in our app, and not necessarily the event stream route.

var SSE = require('express-sse');
var sse = new SSE();
app.get('/stream', sse.init);
// we can always now call sse.send from anywhere the sse variable is available, and see the result in our stream.
let content = 'Test data at ' + JSON.stringify(Date.now());
sse.send(content);

Copy link to clipboardSSE and Ably

Ably is an enterprise-ready pub/sub messaging platform. We make it easy to efficiently design, quickly ship, and seamlessly scale critical realtime functionality delivered directly to end-users. Everyday we deliver billions of realtime messages to millions of users for thousands of companies.

We power the apps people, organizations, and enterprises depend on everyday like Lightspeed System’s realtime device management platform for over seven million school-owned devices, Vitac’s live captioning for 100s of millions of multilingual viewers for events like the Olympic Games, and Split’s realtime feature flagging for one trillion feature flags per month.

We’re the only pub/sub platform with a suite of baked-in services to build complete realtime functionality: presence shows a driver’s live GPS location on a home-delivery app, history instantly loads the most recent score when opening a sports app, stream resume automatically handles reconnection when swapping networks, and our integrations extend Ably into third-party clouds and systems like AWS Kinesis and RabbitMQ.  With 25+ SDKs we target every major platform across web, mobile, and IoT.

Our platform is mathematically modelled around Four Pillars of Dependability so we’re able to ensure messages don’t get lost while still being delivered at low latency over a secure, reliable, and highly available global edge network.

Developers from startups to industrial giants choose to build on Ably because they simplify engineering, minimize DevOps overhead, and increase development velocity.

While SSE is a lightweight protocol from both an implementation and usage standpoint, there are still infrastructure considerations when operating event-driven streams at scale. And, as we continue to move toward a world of event-driven data streams, apps, services, and APIs, SSE won’t always be the right choice of protocol or fulfill your streaming needs. Now more than ever, building on a platform that can future-proof your infrastructure needs is something even the tech giants know is the best choice.

As such, here at Ably, we’ve always advocated for and supported open realtime protocols. By using Ably, developers can stream data using the open protocols of their choice, including Server-Sent Events, WebSockets, MQTT, STOMP, AMQP, and other proprietary realtime protocols, with minimal DevOps and infrastructure overheads.

Copy link to clipboardReferences and further reading