A one-way stream with server-sent events

Server-sent events (SSEs) send automatic updates from a server to a client over a single HTTP connection. Once that connection is established, the server can initiate data transmission whenever it needs to—a useful pattern for push notifications and other one-way updates.

SSEs deliver information in only one direction, so you won't receive updates from the client. For two-way communication, you'd pair SSE with XMLHttpRequest for client-to-server data.

Why SSEs matter

Before SSEs, developers relied on techniques that created unnecessary overhead:

  • Polling: The client repeatedly requests data from the server. Each request waits for a response, and if no data is available the server returns an empty response. Constant polling generates significant HTTP overhead.
  • Long polling (Hanging GET / COMET): The server holds the request open when no data is available, until new data is ready. It then responds, closes the connection, and the cycle repeats. This often required hacks like appending script tags to an infinite iframe.

SSEs were designed to be more efficient. With SSE, the server pushes data to the app whenever it wants—no initial client request needed for each update. SSE opens a single unidirectional channel, and because the browser handles SSE natively, the client just listens for messages instead of managing polling logic.

SSE versus WebSockets

WebSockets provide bi-directional, full-duplex communication. That two-way channel works best for games, messaging apps, and scenarios needing near real-time updates in both directions.

Sometimes you only need one-way server-to-client data: friend status updates, stock tickers, news feeds, or updates to a client-side Web SQL Database or IndexedDB object store. SSE runs over HTTP with no special protocol or server setup. WebSockets require dedicated servers that handle the full-duplex protocol.

SSEs also include built-in features WebSockets lack by design: automatic reconnection, event IDs, and the ability to send arbitrary named events.

Create an EventSource with JavaScript

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

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

Set a handler for the message event, plus optional handlers 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 the server pushes updates, the onmessage handler fires and the new data is available in the e.data property. One notable behavior: the browser automatically reconnects roughly three seconds after the connection closes. The server can control that timeout (covered below).

The event stream format

An event stream is plain text served with text/event-stream as the Content-Type. The basic format contains a data: line, the message, then two newline characters to end the stream:

data: My message\n\n

Multi-line data: For longer messages, break them across multiple data: lines. Consecutive lines starting with data: are treated as a single message—one message event fires. Each line ends in a single "\n" except the final one, which ends with two:

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

This produces "first line\nsecond line" in e.data.

Send JSON: Multiple lines are useful for sending JSON without breaking its syntax:

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

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: Include a line starting with id: to send a unique event ID:

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

This enables the browser to track the last event fired. If the connection drops, the browser sends the Last-Event-ID HTTP header with its reconnection request so the server can determine which event is appropriate to send next. On the client, the message event exposes e.lastEventId.

Control the reconnection timeout: Alter the default ~3 second reconnect delay with a retry: line specifying milliseconds:

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

The example above attempts a reconnect after 10 seconds.

Named events: An event source can emit different event types using an event: line with a unique name. The client adds event listeners for each specific type:

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

Client-side listeners:

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}`);
};

Minimal server implementations

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()));
?>

A similar Node.js implementation using an Express 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();
});

And the accompanying Node.js client page:

<!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

Both client and server can cancel a stream to prevent auto-reconnection.

From the client, call:

source.close();

From the server, respond with a non-text/event-stream Content-Type or return any HTTP status other than 200 OK (like 404 Not Found).

Security context

Requests generated by EventSource follow the same-origin policy, just like other network APIs such as fetch. To expose an SSE endpoint to different origins, enable Cross Origin Resource Sharing (CORS) on the server.