Server Sent Events
Sometimes you don’t need a full two-way channel. You just want the server to keep talking to you: new chat messages, live scores, a ticker of stock prices, progress on a long job. The data flows one way, from server to browser, and it never stops mid-page.
The Server-Sent Events specification gives you a built-in class for exactly that: EventSource. It opens a connection to the server and holds it open, receiving events as the server writes them.
Like WebSocket, the connection stays alive. But the two solve different problems, and the differences matter when you pick one.
WebSocket |
EventSource |
|---|---|
| Bi-directional: client and server both send messages | One-directional: only the server sends |
| Binary and text data | Text only |
| The WebSocket protocol (its own upgrade handshake) | Plain HTTP |
Put bluntly, EventSource can do less than WebSocket. So why reach for it?
Because less is often enough, and simpler is a feature. Plenty of applications never send anything back to the server over the live channel; they only listen. If all you do is stream a feed of updates down to the page, WebSocket is more machinery than the job calls for. On top of that, EventSource reconnects on its own — behavior you’d have to hand-write with WebSocket — and it rides on ordinary HTTP, so there’s no new protocol for your proxies, load balancers, or firewalls to learn.
Getting messages
To start listening, create new EventSource(url):
let eventSource = new EventSource("/events/subscribe");
The browser connects to url and keeps the connection open, waiting for the server to write events into it.
On the other end, the server must respond with status 200 and the header Content-Type: text/event-stream. Then, instead of closing the response, it holds the connection open and writes messages into it in a specific line-based format:
data: Message 1
data: Message 2
data: Message 3
data: of two lines
The rules of that format are short:
- The message text follows
data:. The space after the colon is optional. - Messages are separated by a double line break,
\n\n. - To put a real line break
\ninside one message, send anotherdata:line. The third message above is a single message spanning two lines.
data: of two lines
In real code, you rarely rely on multi-line data:. Complex payloads travel as JSON, where a line break is already encoded as \n inside the string. So a single data: line carries the whole thing:
data: {"user":"Maya","message":"First line\n Second line"}
That lets you treat one data: block as exactly one message, which keeps the client simple.
Every message the browser reads fires a message event. Its text is on event.data:
let eventSource = new EventSource("/events/subscribe");
eventSource.onmessage = function(event) {
console.log("New message", event.data);
// logs 3 times for the data stream above
};
// same thing with addEventListener:
// eventSource.addEventListener('message', ...)
To feel how that parsing works, edit the raw stream below and press Parse. Consecutive data: lines fold into one message joined by \n, blank lines split messages apart, and id: / event: become metadata rather than body text:
Cross-origin requests
EventSource can connect to a different origin, the same as fetch and other networking APIs. Any URL works:
let source = new EventSource("https://another-site.com/events");
The remote server receives an Origin header and must answer with Access-Control-Allow-Origin for the connection to go through. Without that header, the browser blocks it.
By default the request carries no cookies or HTTP auth. To include them, pass the withCredentials option:
let source = new EventSource("https://another-site.com/events", {
withCredentials: true
});
The cross-origin rules are the same ones covered in Fetch: Cross-Origin Requests, so the mental model you already have for CORS carries straight over.
Reconnection
When you create an EventSource, it connects. If that connection later drops, it reconnects on its own. You write no retry loop, no exponential backoff, no “is it dead?” check. That auto-recovery is one of the strongest reasons to choose it.
There’s a short pause between a drop and the next attempt — a few seconds by default. The server can suggest its own delay with a retry: field, given in milliseconds:
retry: 15000
data: Hello, I set the reconnection delay to 15 seconds
retry: can ride along with data, as above, or arrive on its own as a standalone message. The browser treats the number as a floor, not a hard rule. It waits at least that long. It may wait longer — for instance, if the operating system reports there’s no network right now, the browser can hold off until connectivity returns, then try.
Two parties can end the cycle deliberately:
- The server stops the browser from reconnecting by answering with HTTP status
204 No Content. The browser takes that as “we’re done” and doesn’t try again. - The browser closes from its side by calling
eventSource.close():
let eventSource = new EventSource(...);
eventSource.close();
Reconnection also stops on a broken response. If the server replies with the wrong Content-Type, or with any status other than 301, 307, 200, or 204, the browser gives up: it fires an "error" event and does not reconnect.
Message id
Networks fail at awkward moments. When a connection breaks mid-stream, neither side can be sure which messages actually arrived. Did message 7 land before the drop, or not? Guessing means either losing a message or replaying one.
The fix is to tag each message with an id: field:
data: Message 1
id: 1
data: Message 2
id: 2
data: Message 3
data: of two lines
id: 3
When the browser reads a message carrying id:, it does two things:
- Stores that value on
eventSource.lastEventId. - On the next reconnection, sends it back to the server in a
Last-Event-IDheader.
That header is the recovery handshake. The server reads “the client last saw id 2,” and resends everything after id 2. No gaps, no duplicates.
Connection status: readyState
An EventSource object tracks its own state on the readyState property, which is always one of three values:
EventSource.CONNECTING = 0; // connecting or reconnecting
EventSource.OPEN = 1; // connected
EventSource.CLOSED = 2; // connection closed
A newly created object starts at CONNECTING (0), and it returns to CONNECTING any time the link goes down and a reconnect is pending. Read this property whenever you need to know where the connection stands — for example, inside an error handler to tell “temporarily reconnecting” apart from “dead for good.”
Event types
Out of the box, an EventSource object fires three events:
message— a message arrived; its text is onevent.data.open— the connection just opened.error— the connection couldn’t be established or was lost, for example the server returned HTTP500.
The server isn’t limited to plain messages. It can name a custom event type with an event: field placed before the data::
event: join
data: Theo
data: Hello
event: leave
data: Theo
Here the first block is a join event, the middle block has no event: so it’s an ordinary message, and the last is a leave event.
Custom event names can’t be caught with onmessage — that handler only ever fires for the default message type. For named events you must register with addEventListener:
eventSource.addEventListener('join', event => {
alert(`Joined ${event.data}`);
});
eventSource.addEventListener('message', event => {
alert(`Said: ${event.data}`);
});
eventSource.addEventListener('leave', event => {
alert(`Left ${event.data}`);
});
Full example
Below is a server that streams 1, 2, 3, then bye, and drops the connection. Watch the browser reconnect on its own afterward, with no client code arranging it.
Since a live server can’t run inside this page, the demo below simulates an EventSource: a mock server pushes 1, 2, 3, then bye, and then drops. Watch the status badge fall back to CONNECTING and the stream restart on its own. Press Close to call close() and end the cycle for good.
Summary
EventSource opens a persistent connection for you and lets the server push messages down it over ordinary HTTP.
What you get built in:
- Automatic reconnect, with a tunable
retrydelay set by the server. - Message ids for resuming cleanly — the last id is replayed in the
Last-Event-IDheader on reconnect. - Live connection state on the
readyStateproperty.
Those features are why EventSource stands in for WebSocket in many cases. WebSocket is lower-level and gives you none of this out of the box; you’d build reconnection and resume yourself. When the data only flows one way, EventSource is frequently all you need.
It works in every modern browser (not Internet Explorer).
The constructor:
let source = new EventSource(url, [credentials]);
The second argument accepts a single option, { withCredentials: true }, which sends credentials on cross-origin requests. The cross-origin security model is the same one used by fetch and the other networking methods.
Properties of an EventSource object
readyStateThe current connection state:
EventSource.CONNECTING (=0),EventSource.OPEN (=1), orEventSource.CLOSED (=2).
lastEventIdThe last received
id. The browser replays it in theLast-Event-IDheader when it reconnects.
Methods
close()Closes the connection for good.
Events
messageA message arrived; the text is on
event.data.
openThe connection is established.
errorFired on any error, both a lost connection (auto-reconnect will follow) and fatal failures. Check
readyStateto tell whether a reconnect is being attempted.
The server can assign a custom event name with event:. Handle those with addEventListener, never on<event>.
Server response format
The server sends messages separated by \n\n. Each message may carry these fields:
data:— the message body. Severaldata:lines in a row become one message, joined with\n.id:— updateslastEventId, which is sent back inLast-Event-IDon reconnect.retry:— recommended reconnect delay in milliseconds. There’s no way to set it from JavaScript.event:— the event name; it must come beforedata:.
A message may include one or more of these fields in any order, though id: conventionally comes last.