Eric Bidelman [X](https://twitter.com/ebidel) [GitHub](https://github.com/ebidel) [Homepage](http://ericbidelman.com/)

<br />

*[Server-sent events](https://developer.mozilla.org/docs/Web/API/Server-sent_events)*
(SSEs) send automatic updates to a client from a server, with an HTTP
connection. Once the connection is established, servers can initiate data
transmission.

You may want to use SSEs to send push notifications from your web app.
SSEs send information in one direction, thus you won't receive updates from the
client.

The concept of SSEs may be familiar. A web app "subscribes" to a stream of
updates generated by a server and, whenever a new event occurs, a notification
is sent to the client. But to really understand server-sent events, we need to
understand the limitations of its AJAX predecessors. This includes:

- **Polling**: The application repeatedly polls a server for data. This technique
  is used by a majority of AJAX applications. With the HTTP protocol, fetching
  data revolves around a request and response format. The client makes a request
  and waits for the server to respond with data. If none is available, an empty
  response is returned. Extra polling creates greater HTTP overhead.

- **Long polling (Hanging GET / COMET)**: If the server does not have data
  available, the server holds the request open until new data is made available.
  Hence, this technique is often referred to as a "Hanging GET". When
  information becomes available, the server responds, closes the connection,
  and the process is repeated. Thus, the server is constantly responding with
  new data. To set this up, developers typically use hacks such as appending
  script tags to an 'infinite' iframe.

Server-sent events, have been designed from the ground up to be efficient.
When communicating with SSEs, a server can push data to your
app whenever it wants, without the need to make an initial request. In other words,
updates can be streamed from server to client as they happen. SSEs
open a single unidirectional channel between server and client.

The main difference between server-sent events and long-polling is that SSEs
are handled directly by the browser and the user just has to listen for messages.

## Server-sent events versus WebSockets

Why would you choose server-sent events over WebSockets? Good question.

[WebSockets](https://web.dev/articles/websockets-basics) has a rich protocol with
bi-directional, full-duplex communication. A two-way channel is better for
games, messaging apps, and any use case where you need near real-time updates in
both directions.

However, sometimes you only need one-way communication from a server.
For example, when a friend updates their status, stock tickers, news feeds, or
other automated data push mechanisms. In other words,
an update to a client-side Web SQL Database or IndexedDB object store.
If you need to send data to a server, `XMLHttpRequest` is always a friend.

SSEs are sent over HTTP. There's no special protocol or server
implementation to get working. WebSockets require full-duplex
connections and new WebSocket servers to handle the protocol.

In addition, server-sent events have a variety of features that WebSockets lack
by design, including automatic reconnection, event IDs, and an ability to send
arbitrary events.

## Create an EventSource with JavaScript

To subscribe to an event stream, create an `EventSource` object and pass it the
URL of your stream:

    const source = new EventSource('stream.php');

> [!NOTE]
> **Note:** If the URL passed to the `EventSource` constructor is an absolute URL, its origin (scheme, domain, port) must match that of the calling page.

Next, set up a handler for the `message` event. You can optionally
listen for `open` and `error`:

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

    source.addEventListener('open', (e) => {
      // Connection was opened.
    });

    source.addEventListener('error', (e) => {
      if (e.readyState == EventSource.CLOSED) {
        // Connection was closed.
      }
    });

When updates are pushed from the server, the `onmessage` handler fires
and new data is be available in its `e.data` property. The magical part is
that whenever the connection is closed, the browser automatically reconnects to the
source after \~3 seconds. Your server implementation can even have [control over
this reconnection timeout](https://web.dev/articles/eventsource-basics#control-the-reconnection-timeout).

That's it. Your client can now process events from `stream.php`.

## Event stream format

Sending an event stream from the source is a matter of constructing a
plain text response, served with a `text/event-stream` Content-Type,
that follows the SSE format.
In its basic form, the response should contain a `data:` line, followed by your
message, followed by two "\\n" characters to end the stream:

    data: My message\n\n

## Multi-line data

If your message is longer, you can break it up by using multiple `data:` lines.
Two or more consecutive lines beginning with `data:` are treated as a
single piece of data, meaning only one `message` event is fired.

Each line should end in a single "\\n" (except for the last, which should end
with two). The result passed to your `message` handler is a single string
concatenated by newline characters. For example:

    data: first line\n
    data: second line\n\n</pre>

This produces "first line\\nsecond line" in `e.data`. One could then use
`e.data.split('\n').join('')` to reconstruct the message sans "\\n" characters.

### Send JSON data

Using multiple lines helps you send JSON without breaking syntax:

    data: {\n
    data: "msg": "hello world",\n
    data: "id": 12345\n
    data: }\n\n

And possible client-side code to handle that stream:

    source.addEventListener('message', (e) => {
      const data = JSON.parse(e.data);
      console.log(data.id, data.msg);
    });

### Associate an ID with an event

You can send a unique ID with an stream event by including a line starting with
`id:`:

    id: 12345\n
    data: GOOG\n
    data: 556\n\n

Setting an ID lets the browser keep track of the last event fired so that if, the
connection to the server is dropped, a special HTTP header (`Last-Event-ID`) is
set with the new request. This lets the browser determine which event is appropriate to fire.
The `message` event contains a `e.lastEventId` property.

### Control the reconnection-timeout

The browser attempts to reconnect to the source roughly 3 seconds
after each connection is closed. You can change that timeout by including a
line beginning with `retry:`, followed by the number of milliseconds
to wait before trying to reconnect.

The following example attempts a reconnect after 10 seconds:

    retry: 10000\n
    data: hello world\n\n

### Specify an event name

A single event source can generate different types events by including an
event name. If a line beginning with `event:` is present,
followed by a unique name for the event, the event is associated with that name.
On the client, an event listener can be setup to listen to that particular event.

For example, the following server output sends three types of events,
a generic 'message' event, 'userlogon', and 'update' event:

    data: {"msg": "First message"}\n\n
    event: userlogon\n
    data: {"username": "John123"}\n\n
    event: update\n
    data: {"username": "John123", "emotion": "happy"}\n\n

With event listeners setup on the client:

    source.addEventListener('message', (e) => {
      const data = JSON.parse(e.data);
      console.log(data.msg);
    });

    source.addEventListener('userlogon', (e) => {
      const data = JSON.parse(e.data);
      console.log(`User login: ${data.username}`);
    });

    source.addEventListener('update', (e) => {
      const data = JSON.parse(e.data);
      console.log(`${data.username} is now ${data.emotion}`);
    };

## Server examples

Here's a basic server implementation in PHP:

    <?php
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache'); // recommended to prevent caching of event data.

    /**
    * Constructs the SSE data format and flushes that data to the client.
    *
    * @param string $id Timestamp/id of this connection.
    * @param string $msg Line of text that should be transmitted.
    **/

    function sendMsg($id, $msg) {
      echo "id: $id" . PHP_EOL;
      echo "data: $msg" . PHP_EOL;
      echo PHP_EOL;
      ob_flush();
      flush();
    }

    $serverTime = time();

    sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));
    ?>

Here's a similar implementation on [Node JS](http://nodejs.org) using an
[Express](https://expressjs.com/) handler:

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

        // Sends an event to the client where the data is the current date,
        // then schedules the event to happen again after 5 seconds.
        const sendEvent = () => {
            const data = (new Date()).toLocaleTimeString();
            res.write("data: " + data + '\n\n');
            setTimeout(sendEvent, 5000);
        };

        // Send the initial event immediately.
        sendEvent();
    });

> [!NOTE]
> **Note:** The EventSource API is limited to making GET requests to the server. It's not possible to use POST requests with the EventSource API.

### sse-node.html

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
      </head>
      <body>
        <script>
        const source = new EventSource('/events');
        source.onmessage = (e) => {
            const content = document.createElement('div');
            content.textContent = e.data;
            document.body.append(content);
        };
        </script>
      </body>
    </html>

## Cancel an event stream

Normally, the browser auto-reconnects to the event source when the connection
is closed, but that behavior can be canceled from either the client or server.

To cancel a stream from the client, call:

    source.close();

To cancel a stream from the server, respond with a non `text/event-stream`
`Content-Type` or return an HTTP status other than `200 OK`
(such as `404 Not Found`).

Both methods prevent the browser from re-establishing the connection.

## A word on security

Requests generated by EventSource are subject to the same-origin policies as
other network APIs like fetch. If you need the SSE endpoint on your server to be
accessible from different origins, read how to enable with
[Cross Origin Resource Sharing (CORS)](https://web.dev/articles/cross-origin-resource-sharing).