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.

A 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().

What 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.

How 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 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.


How SSE works from connection request to close.

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);
};

Why 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.

When 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

Things 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 information 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 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.

SSE 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 broswers your application or service supports.


Browers that support SSE as of June 2019.

Browers that support SSE as of June 2019.

It’s also worth checking EventSource support. See the example below using polyfill:

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

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

Using 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), and they send it on the clients. 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.

How to use Server-Sent Events 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 when using 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, 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);

Server-Sent Events support and Ably

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 futureproof your infrastructure needs is something even the tech giants know is the best choice.

Ably is a Data Stream Network. Developers use our cloud platform and global network to power realtime apps and services, internal microservices, and to create streaming APIs. Our mission is to deliver a best-in-class, interoperable realtime platform developers can trust now and in the future so they can do what they do best: build without worrying about infrastructure.

As such we’ve always advocated for and supported open realtime protocols. Using the Ably Adapter developers can stream data using the open protocols of their choice, including Server-Sent Events, WebSockets, MQTT, STOMP, AMQP, and other proprietary realtime protocols. We’ll continue to add support for new protocols, including gRPC, which is currently in development.

For more information about Ably and how we can help you gain a realtime advantage, get in touch or check out our blog.

References and further reading


Ready to get started?

Our free plan includes 3m messages per month, 100 peak connections, 100 peak channels, and loads of features.