Web Platform APIs · 9 / 10
lesson 9

Real-time: Server-Sent Events

When SSE beats WebSockets. EventSource, retry semantics, and the simpler half of real-time.

~ 12 min read·lesson 9 of 10
0 / 10

You read the WebSockets lesson and thought, "Wait, this is a lot of code for something that just receives updates from a server." You're right. Half the time you don't need a phone call — you need the server to push updates to the client, and that's it. No client-to-server messages, no binary frames, no handshake protocol to debug. Server-Sent Events is that simpler half: a one-way HTTP stream the server holds open, sending text messages whenever there's news.

If WebSockets are a phone call, SSE is a radio. The station broadcasts; you tune in. Listeners can't talk back over the radio — but for a lot of cases, you don't need to.

What an SSE stream looks like

An SSE response is a normal HTTP response with Content-Type: text/event-stream. The body never finishes until the connection closes. It's plain UTF-8 text with a tiny format: each message is one or more key: value lines, terminated by a blank line.

stream.txt
data: hello

data: another message

event: comment
data: {"id":42,"text":"nice post"}

That's the whole protocol. Three keys you'll meet:

  • data: — the message payload (a string; you parse JSON yourself).
  • event: — an optional event name (default: "message").
  • id: — an optional ID the browser remembers, so it can resume after a reconnect.

The browser parses these for you; on the JS side, you just receive events.

EventSource on the client

new EventSource(url) opens the connection and starts listening. The default message event fires for every entry without a custom event: key.

comments.js
const stream = new EventSource("/api/comments/stream");

stream.addEventListener("message", (event) => {
const comment = JSON.parse(event.data);
addComment(comment);
});

stream.addEventListener("error", () => {
/* the browser is already retrying; you can update UI */
setStatus("reconnecting");
});

There's no open event you need to wait for — the moment the connection succeeds, messages start flowing. There's no send method either; this is server-to-client only.

event.data is whatever string the server put after data:. If it's JSON, you parse. If it's plain text, you use it directly. Multi-line data: blocks get joined with newlines automatically.

Tip

EventSource always uses GET. There's no way to send a POST body when opening the stream — if you need to pass parameters, put them in the URL or set them as cookies/headers via the request that authenticated the user.

Named events

When the server uses event: someName, you listen for that name instead of message.

named.js
stream.addEventListener("comment", (event) => {
addComment(JSON.parse(event.data));
});
stream.addEventListener("typing", (event) => {
showTyping(JSON.parse(event.data));
});
stream.addEventListener("presence", (event) => {
updatePresence(JSON.parse(event.data));
});

This is the SSE equivalent of the type field convention you saw in WebSockets — but the protocol gives it to you for free, and event listeners route automatically. Each handler gets only the events it asked for.

check your understanding
The server sends event: presence\\ndata: ...\\n\\n. Your stream.addEventListener("message", ...) handler never fires. Why?

Built-in reconnect

Here's the killer feature of SSE: reconnection is automatic. If the connection drops, the browser waits a few seconds and retries. If the server sent any id: lines, the browser remembers the last one and includes it in the reconnect request as a Last-Event-ID header — so the server can resume from where it left off.

You write zero code for any of this. The exponential-backoff loop you wrote for WebSockets is built into EventSource.

resume.txt
/* Server-side: */
id: 1042
data: {"text":"...","id":1042}

/* On reconnect, the browser sends: */
GET /api/stream
Last-Event-ID: 1042

The server reads Last-Event-ID and resumes. Combined with named events, this gives you a real-time feed that survives flaky networks with no client code at all.

To stop listening: stream.close(). After that, no more reconnect attempts.

SSE vs WebSockets

The decision tree is short.

  • One-way (server → client) updates? SSE. Notifications, live counters, log tails, an LLM streaming a reply, comments appearing in real time. SSE is shorter to write, gets reconnect for free, and runs over plain HTTP — proxies, load balancers, and CDNs handle it without special configuration.
  • Two-way messaging? WebSockets. Chat, multiplayer games, collaborative editing, anything where the client also sends frequent messages. SSE can't do this — for client-to-server you'd need a separate fetch, which gets clunky fast.
  • Binary data, low-latency frames? WebSockets. SSE is text-only.

A common pattern: SSE for the firehose of server pushes, plus regular fetch for the occasional client action. You get the simplicity of SSE without losing the ability to write to the server when needed.

SSEclientserverWebSocketclientserver
SSE is one-way and HTTP-shaped. WebSockets are two-way and use their own protocol after upgrade.
check your understanding
You're building a notifications widget. The server pushes new notifications; the client doesn't send anything back over the channel (it just opens the stream and listens). Which is the simpler fit?

Try it yourself

check your understanding
The server side sends id: 88 with each event. The connection drops; the browser reconnects. What does the new request include?
check your understanding
You want to stop listening to an SSE stream when the user navigates away. What do you call?
check your understanding
Your app needs to send chat messages from the client AND receive incoming ones in real time. SSE alone, WebSocket alone, or a mix?
← prevnext lesson →
KeepLearningcertificate
for completing
Web Platform APIs
0 of 10 read