Server-Sent Events (SSEs) allow one-way communication from the server to the client. They can be very useful for things like notifications or activity feeds. I’ve recently used them in a project to display output from a background process in the browser.

In the browser, you connect to the server using the EventSource interface and just add event listeners. It is really very easy.

const sseSource = new EventSource('/event-stream');

sseSource.addEventListener('message', (e) => {
    const messageData = e.data;
    // ...
    // ...
});

// When finished with the source close the connection
sseSource.close();

Things on the server-side are a bit more complicated, but not by a lot. There are just very specific things that need to be done. The HTTP connection needs to be kept open. I saw a lot of examples that included req.socket.setTimeout(Infinity) but that isn’t necessary and throws an error at least in Node v8 and above. By default, the connection is kept open on the Node end. You should send a Connection: keep-alive header to ensure the client keeps the connection open as well. A Cache-Control header should be sent with the value no-cache to discourage the data being cached. Finally, the Content-Type needs to be set to text/event-stream.

With all of that done a newline (\n) should be sent to the client and then the events can be sent. Events must be sent as strings, but what is in that string doesn’t matter. JSON strings are perfectly fine.

Event data must be sent in the format data: <DATA TO SEND HERE>\n. The data: portion is important because you can provide IDs and types for the events. An example with both might look like this:

id: 42
event: deepThoughtOutput
data: I have finished computing the answer

It’s important to note that at the end of each line should be a newline character. To signify the end of an event an extra newline character needs to be added as well. For the example above the EventSource listener should be attached to a deepThoughtOutput event instead of the message event.

Multiple data lines are perfectly fine, the one below works without an issue.

data: [
data: "Array Element 1",
data: "Array Element 2",    
data: ]

When IDs are being used there is the Last-Event-ID HTTP header that you may run into. If the connection is broken the client will send the last ID it received in the Last-Event-ID header to allow the events to be resumed from where they were left off. Pay special attention to any polyfill libraries you use in this area. Some use a query string instead of a header.

Below is an example of a node application that uses SSEs.

const express = require('express');

const app = express();

function sseDemo(req, res) {
    let messageId = 0;

    const intervalId = setInterval(() => {
        res.write(`id: ${messageId}\n`);
        res.write(`data: Test Message -- ${Date.now()}\n\n`);
        messageId += 1;
    }, 1000);

    req.on('close', () => {
        clearInterval(intervalId);
    });
}

app.get('/event-stream', (req, res) => {
    // SSE Setup
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
    });
    res.write('\n');

    sseDemo(req, res);
});

app.listen(3000);

Note the close event handler for the request. If you need to clean anything up this is the place to do it. In this example, I stop the interval timer so that it doesn’t continue running needlessly on the server.

When I was implementing SSEs I was doing so on top of an application template someone else had built and not fresh from an example. I was running into an odd issue where my events would only be received by the client after the connection closed. After some digging, I found it was due to the compression setup.

The compression npm package looks at mime-types to determine if the response should be compressed. All text mime-types are compressed by default which means the events being sent were being buffered for compression. I was never getting enough into the buffer for it to be flushed. If you run into this you have a few options. You can disable compression for text/event-stream mime-types, disable compression for your SSE endpoint, or you can call req.flush() after every event to flush the buffer and send your event to the client.

Another important bit of information is authentication. There is no ability to send custom headers through EventSource. If you need to pass a token to the server you should use cookies. In my case, I set an HttpOnly cookie on the server when the user authenticates and then use that to verify their identity for events.

If your client and server are not at the same origin there are some CORS considerations you will need to make that I don’t cover here. If you use a load balancer or proxy you will also need to make sure it won’t close the connection early. The client would try and reconnect, but it would add extra overhead to frequently reconnect.

Below are some additional resources if you want to look into Server-Sent Event in more detail.

Mozilla Developer Network - Using server-sent events

WHATWG Server-Sent Events Living Standard

HTML5 Rocks EventSource tutorial

Server-Sent Events with Node.js (Express) tutorial