Server-Sent Events with Express

Jan 15, 2020

Server-sent events are a new HTTP API for pushing events from the server to the client. Unlike websockets, server-sent events (SSE for short) are built on top of the HTTP protocol, so no need for ws:// URLs or additional npm modules. Server-side events also handle reconnecting automatically, so you don't have to write code to reconnect if the connection is lost.

Getting Started

On the client side, you use the EventSource class to connect to a server-side event source. The client side is easy: just point the EventSource class to an Express route that's configured to handle SSE and add an event listener.

<html>
  <body>
    <div id="content"></div>

    <script type="text/javascript">
      const source = new EventSource('/events');

      source.addEventListener('message', message => {
        console.log('Got', message);

        // Display the event data in the `content` div
        document.querySelector('#content').innerHTML = event.data;
      });
    </script>
  </body>
</html>

The Express side is the tricky part. To support SSE, you need to set 3 headers, and then send the headers to the client using flushHeaders():

Once you've called flushHeaders(), you can then start writing events using the res.write() function. The res.write() function writes to the HTTP connection without explicitly ending the HTTP response. Make sure you do not use res.send() or res.end(), because those explicitly end the response.

Below is an example of a standalone Express server that handles SSE with the /events endpoint:

'use strict';

const express = require('express');
const fs = require('fs');

run().catch(err => console.log(err));

async function run() {
  const app = express();

  app.get('/events', async function(req, res) {
    console.log('Got /events');
    res.set({
      'Cache-Control': 'no-cache',
      'Content-Type': 'text/event-stream',
      'Connection': 'keep-alive'
    });
    res.flushHeaders();

    // Tell the client to retry every 10 seconds if connectivity is lost
    res.write('retry: 10000\n\n');
    let count = 0;

    while (true) {
      await new Promise(resolve => setTimeout(resolve, 1000));

      console.log('Emit', ++count);
      // Emit an SSE that contains the current 'count' as a string
      res.write(`data: ${count}\n\n`);
    }
  });

  const index = fs.readFileSync('./index.html', 'utf8');
  app.get('/', (req, res) => res.send(index));

  await app.listen(3000);
  console.log('Listening on port 3000');
}

Run the above server and navigate to http://localhost:3000, you should see the below:


Want to become your team's Express expert? There's no better way to really grok a framework than to write your own clone from scratch. In 15 concise pages, this tutorial walks you through how to write a simplified clone of Express called Espresso. Get your copy!

Espresso supports:
  • Route handlers, like `app.get()` and `app.post()`
  • Express-compatible middleware, like `app.use(require('cors')())`
  • Express 4.0 style subrouters
As a bonus, Espresso also supports async functions, unlike Express.

Get the tutorial and master Express today!

More Express Tutorials