Million Miles Technologies

How to Use Server-sent Events in Node.js — SitePoint


In this article, we’ll explore how to use server-sent events to enable a client to receive automatic updates from a server via an HTTP connection. We’ll also look at why this is useful, and we’ll show practical demonstrations of how to use server-sent events with Node.js.

Table of Contents

Why Server-sent Events Are Useful

The Web is based on request-response HTTP messages. Your browser makes a URL request and a server responds with data. That may lead to further browser requests and server responses for images, CSS, JavaScript etc. The server cannot initiate messages to the browser, so how can it indicate that data has changed? Fortunately, you can add features such as live news bulletins, weather reports, and stock prices with server-sent events.

Implementing live data updates using standard web technologies has always been possible:

  • The 1990s Web used a full-page or frame/iframe refresh.
  • The 2000s Web introduced Ajax, which could use long polling to request data and update the appropriate DOM elements with new information.

Neither option is ideal, since the browser must trigger a refresh. If it makes requests too often, no data will have changed so both the browser and server do unnecessary work. If it makes requests too slowly, it may miss an important update and the stock price you’re watching has already crashed!

Server-sent events (SSE) allow a server to push data to the browser at any time:

  • The browser still makes the initial request to establish a connection.
  • The server returns an event-stream response and keeps the connection open.
  • The server can use this connection to send text messages at any point.
  • The incoming data raises a JavaScript event in the browser. An event handler function can parse the data and update the DOM.

In essence, SSE is an unending stream of data. Think of it as downloading an infinitely large file in small chunks that you can intercept and read.

SSE was first implemented in 2006 and all major browsers support the standard. It’s possibly less well known than WebSockets, but server-sent events are simpler, use standard HTTP, support one-way communication, and offer automatic reconnection. This tutorial provides example Node.js code without third-party modules, but SSE is available in other server-side languages including PHP.

Server-sent Events Quick Start

The following demonstration implements a Node.js web server which outputs a random number between 1 and 1,000 at a random interval of at least once every three seconds.

You can find our Node.js SSE demonstration here.

The code uses the standard Node.js http and url modules for creating a web server and parsing URLs:

import http from "node:http";
import url from "node:url";

The server examines the incoming URL request and reacts when it encounters a /random path:

const port = 8000;
http.createServer(async (req, res) => {
  
  const uri = url.parse(req.url).pathname;
  
  switch (uri) {
    case "/random":
      sseStart(res);
      sseRandom(res);
      break;
  }
}).listen(port);
console.log(`server running: http://localhost:${port}\n\n`);

It initially responds with the SSE HTTP event-stream header:


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

Another function then sends a random number and calls itself after a random interval has elapsed:


function sseRandom(res) {
  res.write("data: " + (Math.floor(Math.random() * 1000) + 1) + "\n\n");
  setTimeout(() => sseRandom(res), Math.random() * 3000);
}

If you run the code locally, you can test the response using cURL in your terminal:

$> curl -H Accept:text/event-stream http://localhost:8000/random
data: 481
data: 127
data: 975

Press Ctrl | Cmd and C to terminate the request.

The browser’s client-side JavaScript connects to the /random URI using an EventSource object constructor:


const source = new EventSource("/random");

Incoming data triggers a message event handler where the string following data: is available in the event object’s .data property:

source.addEventListener('message', e => {
  console.log('RECEIVED', e.data);
});

Important notes

  • Like Fetch(), the browser makes a standard HTTP request, so you may need to handle CSP, CORS and optionally pass a second { withCredentials: true } argument to the EventSource constructor to send cookies.
  • The server must retain individual res response objects for every connected user to send them data. It’s achieved in the code above by passing the value in a closure to the next call.
  • Message data can only be a string (perhaps JSON) sent in the format data: \n\n. The terminating carriage returns are essential.
  • The server can terminate an SSE response at any time with res.end(), but…
  • When a disconnect occurs, the browser automatically attempts to reconnect; there’s no need to write your own reconnection code.

Advanced Server-sent Events

SSE requires no more code than that shown above, but the following sections discuss further options.

One vs many SSE channels

A server could provide any number of SSE channel URLs. For example:

  • /latest/news
  • /latest/weather
  • /latest/stockprice

This may be practical if a single page shows one topic, but less so if a single page shows news, weather, and stock prices. In that situation, the server must maintain three connections for each user, which could lead to memory problems as traffic increases.

An alternative option is to provide a single endpoint URL, such as /latest, which sends any data type on one communication channel. The browser could indicate the topics of interest in the URL query string — for example, /latest?type=news,weather,stockprice — so the server can limit SSE responses to specific messages.

Sending different data on a single channel

Messages from the server can have an associated event: passed on the line above the data: to identify specific types of information:

event: news
data: SSE is great!
event: weather
data: { "temperature": "20C", "wind": "10Kph", "rain": "25%" }
event: stock
data: { "symbol": "AC", "company": "Acme Corp", "price": 123.45, "increase": -1.1 }

These will not trigger the client-side "message" event handler. You must add handlers for each type of event. For example:


source.addEventListener('news', e => {
  document.getElementById('headline')
    .textContent = e.data;
});

source.addEventListener('weather', e => {
  const w = JSON.parse(e.data);
  document.getElementById('weather')
    .textContent = `${ w.temperature } with ${ w.wind } wind`;
});

source.addEventListener('stock', e => {
  const s = JSON.parse(e.data);
  document.getElementById(`stock-${ s.symbol }`)
    .textContent = `${ s.share }: ${ s.price } (${ s.increase }%)`;
});

Using data identifiers

Optionally, the server can also send an id: after a data: line:

event: news
data: SSE is great!
id: 42

If the connection drops, the browser sends the last id back to the server in the Last-Event-ID HTTP header so the server can resend any missed messages.

The most recent ID is also available client-side in the event object’s .lastEventId property:


source.addEventListener('news', e => {
  console.log(`last ID: ${ e.lastEventId }`);
  document.getElementById('headline')
    .textContent = e.data;
});

Specifying retry delays

Although reconnection is automatic, your server may know that new data is not expected for a specific period, so there’s no need to retain an active communication channel. The server can send a retry: response with a milliseconds value either on its own or as part of a final message. For example:

retry: 60000
data: Please don't reconnect for another minute!

On receipt, the browser will drop the SSE connection and attempt to reconnect after the delay period has elapsed.

Other event handlers

As well as "message" and named events, you can also create "open" and "error" handlers in your client-side JavaScript.

An "open" event triggers when the server connection is established. It could be used to run additional configuration code or initialize DOM elements:

const source = new EventSource('/sse1');
source.addEventListener('open', e => {
  console.log('SSE connection established.');
});

An "error" event triggers when the server connection fails or terminates. You can examine the event object’s .eventPhase property to check what happened:

source.addEventListener('error', e => {
    if (e.eventPhase === EventSource.CLOSED) {
      console.log('SSE connection closed');
    }
    else {
      console.log('error', e);
    }
});

Remember, there’s no need to reconnect: it occurs automatically.

Terminating SSE communication

The browser can terminate an SSE communication using the EventSource object’s .close() method. For example:

const source = new EventSource('/sse1');

setTimeout(() => source.close(), 3_600_000);

The server can terminate the connection by:

  1. firing res.end() or sending a retry: delay, then
  2. returning an HTTP status 204 when the same browser attempts to reconnect.

Only the browser can re-establish a connection by creating a new EventSource object.

Conclusion

Server Side Events provide a way to implement live page updates which are possibly easier, more practical, and more lightweight than Fetch()-based Ajax polling. The complexity is at the server end. You must:

  1. maintain all user’s active connections in memory, and
  2. trigger data transmissions when something changes.

But this is fully under your control, and scaling should be no more complex than any other web application.

The only drawback is that SSE doesn’t allow you to send messages from the browser to the server (apart from the initial connection request). You could use Ajax, but that’s too slow for apps such as action games. For proper two-way communication, you require WebSockets. Check out How to Use WebSockets in Node.js to Create Real-time Apps to learn more!

Related blogs