W3docs

JavaScript Server-Sent Events (EventSource)

Learn JavaScript Server-Sent Events with the EventSource API — receive a one-way stream of updates from the server over HTTP.

Server-Sent Events (SSE) are a standard way for a server to push a continuous, one-way stream of text updates to the browser over a single, long-lived HTTP connection. Instead of the client repeatedly polling an endpoint with fetch to ask "is there anything new?", the connection stays open and the server writes messages whenever it has something to send. The browser exposes this through the built-in EventSource interface, so you do not need any external library to consume a stream.

SSE shines when updates flow in one direction — from server to client. Live news feeds, notifications, build or upload progress bars, monitoring dashboards, and stock tickers are all natural fits.

Receiving a stream with EventSource

Opening a stream takes a single line. You create an EventSource pointed at a server endpoint, then attach handlers for the events it fires.

const es = new EventSource('/events');

es.onmessage = (event) => {
  console.log('New message:', event.data);
};

es.onopen = () => {
  console.log('Connection opened');
};

es.onerror = (error) => {
  console.log('Connection error:', error);
};

A few things are worth noting:

  • The connection opens as soon as you construct the EventSource.
  • onmessage fires for every default (unnamed) message the server sends. The payload is always a string, available on event.data.
  • onopen fires once when the connection is established (and again after a reconnect).
  • onerror fires when the connection drops or fails. Unlike a failed fetch, this does not necessarily mean you should give up — the browser will usually try to reconnect on its own (more on that below).

Here is a slightly more complete example that parses JSON payloads and updates the page:

const es = new EventSource('/notifications');

es.onmessage = (event) => {
  const data = JSON.parse(event.data);
  const li = document.createElement('li');
  li.textContent = data.message;
  document.querySelector('#feed').append(li);
};

The wire format

The server replies with the text/event-stream content type and writes plain UTF-8 text in a simple line-based format. Each message is a block of field: value lines, and a blank line marks the end of one message.

data: Hello there

data: First line
data: Second line

event: priceUpdate
data: {"symbol":"ACME","price":42.10}

id: 42
data: This message has an id

retry: 10000
data: Reconnect after 10 seconds next time

The recognized fields are:

  • data: — the message payload. If a message has multiple data: lines, they are joined with a newline (\n) before being delivered as event.data.
  • event: — an optional event name. Without it, the message is delivered to onmessage.
  • id: — an optional identifier the browser remembers for reconnection.
  • retry: — the reconnection delay in milliseconds.

Lines starting with a colon (:) are comments and are ignored — servers often send them as keep-alive pings.

Named events

By default every message goes to onmessage. A server can instead label a message with an event: field, and the client listens for that specific name with addEventListener. This lets you route different kinds of updates to different handlers over the same connection.

Given a stream like this:

event: priceUpdate
data: {"symbol":"ACME","price":42.10}

event: newsAlert
data: Markets closed early today

data: a plain default message

You would wire up the client like so:

const es = new EventSource('/market');

es.addEventListener('priceUpdate', (event) => {
  const { symbol, price } = JSON.parse(event.data);
  console.log(`${symbol} is now ${price}`);
});

es.addEventListener('newsAlert', (event) => {
  console.log('News:', event.data);
});

// Messages with no `event:` field still land here.
es.onmessage = (event) => {
  console.log('Default message:', event.data);
};

Automatic reconnection

This is the feature that sets SSE apart from rolling your own streaming with fetch. If the connection drops — a flaky network, a server restart, a proxy timeout — the browser reconnects automatically after a short delay. You do not have to write any retry loop.

To make resuming reliable, the browser keeps track of the last id: it received. On reconnect it sends that value back in a Last-Event-ID request header, so the server can pick up exactly where the stream left off rather than replaying everything.

GET /events HTTP/1.1
Last-Event-ID: 42
Accept: text/event-stream

The server can also tune the delay before the next reconnect by sending a retry: field (in milliseconds):

retry: 5000
data: The browser will wait 5 seconds before reconnecting if dropped
Note

Automatic reconnection only happens while the connection is considered recoverable. If the server responds to a reconnect with an HTTP error such as 204 No Content or a 4xx status, the browser treats the stream as finished and stops trying.

Connection state and closing

An EventSource exposes its current state through the readyState property, which matches three constants:

  • EventSource.CONNECTING (0) — connecting, or waiting to reconnect.
  • EventSource.OPEN (1) — connected and receiving.
  • EventSource.CLOSED (2) — closed and will not reconnect.

When you no longer need the stream, call es.close(). This shuts the connection down immediately, and — importantly — the browser will not reconnect afterward.

const es = new EventSource('/events');

// Stop listening after 30 seconds.
setTimeout(() => {
  es.close();
  console.log('readyState is now', es.readyState); // 2 (CLOSED)
}, 30000);

Cross-origin requests and credentials

By default an EventSource follows the same-origin policy. To stream from a different origin, the server must send the appropriate CORS headers (such as Access-Control-Allow-Origin).

Cookies are not sent on cross-origin requests unless you opt in. Pass the withCredentials option, and make sure the server allows credentials in its CORS configuration:

const es = new EventSource('https://api.example.com/events', {
  withCredentials: true,
});

A note on the server side

SSE is mostly a browser feature, but the server has to cooperate. Whatever language you use, the recipe is the same: respond with Content-Type: text/event-stream, keep the connection open instead of ending the response, and write event-formatted chunks as data becomes available.

Here is a minimal Node.js example to show the shape of it:

import http from 'node:http';

http.createServer((req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  });

  let id = 0;
  const timer = setInterval(() => {
    id += 1;
    res.write(`id: ${id}\n`);
    res.write(`data: The time is ${new Date().toISOString()}\n\n`);
  }, 1000);

  // Stop the timer when the client disconnects.
  req.on('close', () => clearInterval(timer));
}).listen(3000);

Each chunk ends with a blank line (\n\n) to complete the message. The client connected with new EventSource('http://localhost:3000') would receive one update per second.

Limitations

Warning

SSE carries text only (UTF-8) — there is no binary frame type, so you cannot stream raw bytes such as images or audio without encoding them first. It is also strictly one-directional: to send data to the server you still need a normal request via fetch or XMLHttpRequest, or a full-duplex WebSocket connection.

One more practical limit: over HTTP/1.1, browsers cap the number of simultaneous connections to a single origin at around six. Because each EventSource holds one connection open, opening many tabs to the same site can exhaust that budget. This is far less of a concern over HTTP/2, where many streams are multiplexed onto one connection.

SSE vs. WebSockets

SSE and WebSockets both deliver real-time data, but they solve different problems.

Server-Sent EventsWebSockets
DirectionOne-way (server → client)Two-way (full duplex)
ProtocolPlain HTTPws:// / wss:// upgrade
DataText (UTF-8) onlyText and binary
ReconnectionAutomatic, built inImplement it yourself
Best forFeeds, notifications, progressChat, games, collaborative editing

Reach for SSE when the browser only needs to listen — notifications, live feeds, dashboards, progress updates. Its automatic reconnection, plain-HTTP transport, and tiny client API make it the simplest tool for the job. Reach for WebSockets when the client also needs to send frequently, when you need binary data, or when latency per message matters, as in chat apps and multiplayer games.

Info

If your data already lives behind a promise-based async flow, pairing SSE handlers with async/await for any follow-up requests (for example, fetching full details when a lightweight notification arrives) keeps your code clean and readable.

Test Your Knowledge

Practice
Server-Sent Events provide communication in which direction?
Server-Sent Events provide communication in which direction?
Practice
What is a key advantage of EventSource over manually streaming with fetch?
What is a key advantage of EventSource over manually streaming with fetch?
Practice
Which statements about the SSE wire format are true?
Which statements about the SSE wire format are true?
Was this page helpful?