Skip to main content

Command Palette

Search for a command to run...

What are Server-sent events? (SSE)

Demystify with examples

Updated
•11 min read

What is SSE?

Server-sent events (SSE) are a server push technology that enables a client to receive automatic updates from a server via an HTTP connection. They describe how servers can initiate data transmission towards clients once an initial client connection has been established.

Commonly used for message updates or continuous data streams to a browser client, SSE enhances native, cross-browser streaming through a JavaScript API called EventSource, within the HTML5 standard - no external libraries or dependencies required 🙌 and is fully supported by most modern browsers.

Similar to WebSockets, SSE involves server-side code to stream events to the front-end. Unlike WebSockets, SSE is a one-way connection, meaning events cannot be sent from the client to the server.

Other (possibly/probably better) definitions of SSE:

  • W3Schools

    A server-sent event is when a web page automatically gets updates from a server.

    This was also possible before, but the web page would have to ask if any updates were available. With server-sent events, the updates come automatically.

    Examples: Facebook/Twitter updates, stock price updates, news feeds, sport results, etc.

  • Wikipedia

    Server-Sent Events (SSE) is a server push technology enabling a client to receive automatic updates from a server via an HTTP connection, and describes how servers can initiate data transmission towards clients once an initial client connection has been established. They are commonly used to send message updates or continuous data streams to a browser client and designed to enhance native, cross-browser streaming through a JavaScript API called EventSource, through which a client requests a particular URL in order to receive an event stream. The EventSource API is standardized as part of HTML5[1] by the WHATWG. The media type for SSE is text/event-stream.

    All modern browsers support server-sent events: Firefox 6+, Google Chrome 6+, Opera 11.5+, Safari 5+, Microsoft Edge 79+.[2]

  • MDN

    Developing a web application that uses server-sent events is straightforward. You'll need a bit of code on the server to stream events to the front-end, but the client side code works almost identically to websockets in part of handling incoming events. This is a one-way connection, so you can't send events from a client to a server.

So, how does it work?

Basically, it's just an long running HTTP connection that's got a mime-type of text/event-stream. Here's a simple example of how we can set it up using just plain JavaScript - no fancy libraries or complicated abstractions.

We'll use Node.js and the Express.js framework to set up our server-side stuff because it's pretty standard. With just a few lines of code, we can create an SSE endpoint:

app.get("/my-stream", function (req, res) {
  res.status(200).set({
    "content-type": "text/event-stream",
    "cache-control": "no-cache",
    connection: "keep-alive",
  });

  res.write("data: Hello, world!");
});

On the client side, it's easy to subscribe to our SSE:

const source = new EventSource("/my-stream");
source.addEventListener("message", function (message) {
  console.log(message.data);
});

So, what's happening here is that both the server and the client keep the connection alive. The server can close the connection when it deems the transaction is complete via setting the status to 204 No Content. When the client sees this status code in the response, it will stop trying to reconnect (which we can handle with some extra event listeners and try to re-establish the connection manually if required by listening for the readyState of our source to change to CLOSED - see this part of the whatwg spec on connection events and the section on “sse-processing-model”).

Another point to note is that the mime-type doesn't define the content of the messages, just the structure. Think of messages as objects with key/value pairs, described as

Key Value
id the id of the message (integer)
data the actual data (as a string - so you may want to JSON.stringify() your data on the server and JSON.parse() on the client)
event the event type (will usually be the default value of message like in our simple example below, but can be anything like in our more complicated example)
retry milliseconds the user agent should wait before retrying a failed connection

NB: Any keys other than the above four will be ignored according to the spec, so don’t rely on an inconsistent implementation that might allow you to pass more than those 4. Instead, you could create an Object for your data and add a meta entry to put any extra info into.

A simple example, please?

Code in repo: https://github.com/timbryandev/sse-examples/tree/main/01-basic-live-users

Video overview: https://share.zight.com/Wnu827Wg

https://p-jrfbebm0.b2.n0.cdn.zight.com/items/Wnu827Wg/013d324e-aae3-4506-a01a-5e88e36063d4.mp4?v=7c9ce3cfb2f3f03dd7820110e6cd91d3

Let’s pretend we’re building some live UI that displays the number of live users on the site.

First, let’s define our dependencies and build processes - super slim style:

// package.json
{
  "type": "module",
  "scripts": {
    "dev": "nodemon server.js",
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.19.2",
    "nodemon": "^3.1.0"
  }
}

We only require Express.js for our example server, and nodemon for updating the server instance when we make changes to the server.js file.

Secondly, let’s build our Express.js server:

// server.js
import express from "express";
import fs from "fs";

const app = express();
const port = 1337;

// server our index.html page on the "/" route
app.get("/", async (_req, res) => {
  const indexHtml = fs.readFileSync(process.cwd() + "/index.html", "utf8");
  res.send(indexHtml);
});

// setup our SSE route
app.get("/live-users", function (req, res) {
  let intervalId;

  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });

  try {
    // Every 1 seconds, send the user count to the client
    intervalId = setInterval(() => countUsers(res), 1000);
  } catch (err) {
    clearInterval(intervalId);
    console.error(error);
    // we should probably res.send(HTTP status, error.message) to let the
    // consumer know what happened - for now, we'll just dump-and-die
    res.status(500).end();
  }
});

function countUsers(res) {
  const liveUserCount = getUserCount();
  // the client-side on "message" event will fire each time we
  // call res.write
  res.write("data: " + liveUserCount + "\\n\\n");
}

const getUserCount = () => {
  // this would probably be a DB call or something
  // but for now, we'll fake it
  return Math.floor(Math.random() * 100);
};

// serve our Express app on the given port
app.listen(port, () => {
  console.log(`Express app listening on port ${port}`);
});

Finally, in our client, we can have some vanilla JS listening to our endpoint and dumping the messages into the DOM whenever our server tells us there’s a new value to show:

<!-- index.html -->

<!DOCTYPE html>
<html lang="en-GB">
  <head>
    <title>Live Users</title>
  </head>
  <body>
    <h1>Hello, Live Users!</h1>
    <p>
      SSE connection state: <span id="state">[initialising connection]</span>
    </p>
    <p>Live user count: <span id="data">[counting users]</span></p>

    <script>
      const stateContainer = document.querySelector("#state");
      const userCountContainer = document.querySelector("#data");

      // Subscribing to our express endpoint using the native EventSource
      const source = new EventSource("/live-users");

      // This is the magic - whenever a "message" event fires
      // we will pull the data from the event and dump it on the page
      // in our userCountEl container
      source.addEventListener("message", function (e) {
        userCountContainer.innerHTML = e.data;
      });

      // The next two event listeners are just for dumping the state of the
      // connection in the DOM for troubleshooting/loading states
      source.addEventListener("open", function (e) {
        stateContainer.innerHTML = "Connected";
      });

      // If the connection is closed, we will update the state in the DOM
      // and close the connection
      source.addEventListener("error", function (e) {
        if (e.eventPhase == EventSource.CLOSED) {
          source.close();
        }

        if (e.target.readyState == EventSource.CLOSED) {
          stateContainer.innerHTML = "Disconnected";
        }
      });
    </script>
  </body>
</html>

Here is how our response would look in a web browser while we’re consuming the event stream. As you can see in the screenshot, in the Network tab we get a special section on our live-users request called “EventStream” where we can see the history of messages that have been streamed to us from the server.

Note how the Type is the message event we listen for in our client code, and the data is the integer we passed in for our live user count:

![https://share.zight.com/GGuBnzpy](https://p-jrfbebm0.b2.n0.cdn.zight.com/items/GGuBnzpy/e6f99ffd-6357-4cb5-9ab0-a540744a6ad8.jpg?v=a1ff23925e1e57fb3f173eaa72e6e17c align="middle")

https://share.zight.com/GGuBnzpy

Okay, more complicated example?

Code in repo: https://github.com/timbryandev/sse-examples/tree/main/02-complex-smart-home

Video overview:

https://share.zight.com/Z4uNdDb2

https://share.zight.com/Z4uNdDb2

What if we want one endpoint to handle multiple types of event? Maybe having a /weather endpoint where you can subscribe to a post-code for updates? Maybe a /news endpoint where you can subscribe to topics?

How about this:

Let's build a server that streams the current state of the smart electronics in our home, with each room in the house being its own event.

  • The data for each response will contain a JSON response representing the state of the room

  • We’ll use the event to identify the room, this way we can subscribe to rooms individually in the client

Here is what our server might look like:

// server.js
import express from "express";
import fs from "fs";

const app = express();
const port = 1337;

// server our index.html page on the "/" route
app.get("/", async (_req, res) => {
  const indexHtml = fs.readFileSync(process.cwd() + "/index.html", "utf8");
  res.send(indexHtml);
});

// setup our SSE route
app.get("/smart-home", function (req, res) {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });

  try {
    getSmartHomeState(res);
  } catch (err) {
    console.error(error);
    // we should probably res.send(HTTP status, error.message) to let the
    // consumer know what happened - for now, we'll just dump-and-die
    res.status(500).end();
  }
});

const getSmartHomeState = (res) => {
  // Here, we're cheating again by randomly updating values on differing intervals
  // However, you could imagine this taps into some kind of webhook from the smart home
  // and we push the updates to the client

  // Notice here, that the event name is different for each room
  // And we're also sending our JSON data as a string - we need to decode this on the client
  setInterval(() => {
    res.write(
      `event:kitchen\\ndata: {"blinds":"\({randomOpen()}","lights":"\){randomOn()}"}\\n\\n`,
    );
  }, 1000);

  setInterval(() => {
    res.write(
      `event:livingroom\\ndata: {"curtains":"\({randomOpen()}","lights":"\){randomOn()}"}\\n\\n`,
    );
  }, 1800);

  setInterval(() => {
    res.write(
      `event:bedroom\\ndata: {"curtains":"\({randomOpen()}","lights":"\){randomOn()}"}\\n\\n`,
    );
  }, 2600);
};

// Just some helpers for creating randomly changing values
const randomBoolean = () => Math.random() > 0.5;
const randomOpen = () => (randomBoolean() ? "open" : "closed");
const randomOn = () => (randomBoolean() ? "on" : "off");

// serve our Express app on the given port
app.listen(port, () => {
  console.log(`Express app listening on port ${port}`);
});

And here is what our client may look like - you can skip most of the markup and jump straight to the script tag:

<!-- index.html -->
<!doctype html>
<html lang="en-GB">
  <head>
    <title>Welcome, Smart Home</title>
  </head>
  <body>
    <h1>Welcome, Smart Home</h1>
    <main>
      <section>
        <header><h2>Kitchen</h2></header>
        <table>
          <caption>
            Kitchen smart status
          </caption>
          <thead>
            <tr>
              <th>Device</th>
              <th>Status</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>Lights</td>
              <td id="kitchen-lights">[unknown]</td>
            </tr>
            <tr>
              <td>Blinds</td>
              <td id="kitchen-blinds">[unknown]</td>
            </tr>
          </tbody>
          <tfoot>
            <tr>
              <td>Timestamp</td>
              <td id="kitchen-timestamp">[unknown]</td>
            </tr>
          </tfoot>
        </table>
      </section>

      <section>
        <header><h2>Living Room</h2></header>
        <table>
          <caption>
            Living room smart status
          </caption>
          <thead>
            <tr>
              <th>Device</th>
              <th>Status</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>Lights</td>
              <td id="livingroom-lights">[unknown]</td>
            </tr>
            <tr>
              <td>Curtains</td>
              <td id="livingroom-curtains">[unknown]</td>
            </tr>
          </tbody>
          <tfoot>
            <tr>
              <td>Timestamp</td>
              <td id="livingroom-timestamp">[unknown]</td>
            </tr>
          </tfoot>
        </table>
      </section>

      <section>
        <header><h2>Bedroom</h2></header>
        <table>
          <caption>
            Living room smart status
          </caption>
          <thead>
            <tr>
              <th>Device</th>
              <th>Status</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>Lights</td>
              <td id="bedroom-lights">[unknown]</td>
            </tr>
            <tr>
              <td>Curtains</td>
              <td id="bedroom-curtains">[unknown]</td>
            </tr>
          </tbody>
          <tfoot>
            <tr>
              <td>Timestamp</td>
              <td id="bedroom-timestamp">[unknown]</td>
            </tr>
          </tfoot>
        </table>
      </section>
    </main>

    <script>
      // Subscribing to our express endpoint using the native EventSource
      const source = new EventSource("/smart-home");

      // This is the magic - whenever a "message" event fires
      // Except, in this example, the name of the event we are listening to is not "message"
      // it is "kitchen", "livingroom" or "bedroom"
      source.addEventListener("kitchen", function (e) {
        const { blinds, lights } = getJSONFromEvent(e);
        getTableCell("kitchen", "blinds").innerHTML = blinds;
        getTableCell("kitchen", "lights").innerHTML = lights;
        getTableCell("kitchen", "timestamp").innerHTML = e.timeStamp.toFixed(0);
      });

      source.addEventListener("livingroom", function (e) {
        const { curtains, lights } = getJSONFromEvent(e);
        getTableCell("livingroom", "curtains").innerHTML = curtains;
        getTableCell("livingroom", "lights").innerHTML = lights;
        getTableCell("livingroom", "timestamp").innerHTML =
          e.timeStamp.toFixed(0);
      });

      source.addEventListener("bedroom", function (e) {
        const { curtains, lights } = getJSONFromEvent(e);
        getTableCell("bedroom", "curtains").innerHTML = curtains;
        getTableCell("bedroom", "lights").innerHTML = lights;
        getTableCell("bedroom", "timestamp").innerHTML = e.timeStamp.toFixed(0);
      });

      const getTableCell = (room, device) => {
        return document.querySelector(`#\({room}-\){device}`);
      };

      const getJSONFromEvent = (e) => {
        try {
          return JSON.parse(e.data);
        } catch (e) {
          return null;
        }
      };
    </script>

    <style>
      main {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
        gap: 1rem;
      }

      section {
        border: 1px solid #ccc;
        border-radius: 0.5rem;
        padding: 1rem;
      }

      table {
        width: 100%;
      }

      th,
      td {
        text-align: center;
        min-width: 200px;
      }

      td {
        padding: 0.5rem;
      }
    </style>
  </body>
</html>

And here is an example of how it all comes together in the client.

Note in the network tab that in this example, the type is the event that we specified as a room, and we reflect this in our event listeners by listing for kitchen instead of message

Also observe the random order at which the events are firing in - this is because our setInterval is spacing them out to demonstrate the async nature of our streams

![https://share.zight.com/v1u9oQEK](https://p-jrfbebm0.b2.n0.cdn.zight.com/items/v1u9oQEK/7bb63b11-72d7-4979-8722-1cee45cb2b5c.jpg?v=523292412dc2e3cc2726e58ab39a7e9a align="middle")

https://share.zight.com/v1u9oQEK