Server-sent events, usually shortened to SSE, let a server push updates to a browser without the browser having to ask again and again. There is no special protocol involved — SSE is a plain HTTP response that the server simply never closes, writing small text messages to it as new data arrives. The browser reads that stream through the EventSource API, which parses each message and, if the connection ever drops, reconnects automatically. That combination of simplicity and built-in reliability is why SSE quietly became the default transport for AI streaming responses, live tickers, and notification feeds.
What changed in 2026
- AI streaming responses made SSE mainstream knowledge. Every major LLM API streams its output as SSE under the hood, so developers who never touched real-time features directly are now debugging event streams regularly.
- HTTP/2 and HTTP/3 removed the old connection cap. Browsers used to limit six connections per domain over HTTP/1.1, which forced awkward workarounds for pages with multiple SSE streams. That limit does not apply once the connection is multiplexed.
- Framework-level streaming responses got simpler. Next.js route handlers, Remix, and SvelteKit now support returning a streaming
Response directly, cutting the boilerplate an SSE endpoint used to need.
- fetch-based SSE clients became a common alternative to EventSource. The native
EventSource object cannot set custom request headers, so teams needing bearer-token auth increasingly parse the stream manually from a fetch call instead.
How the wire format works
An SSE message is plain text, sent as a series of field: value lines, ended by a blank line:
event: price_update
data: {"symbol":"ACME","price":42.17}
id: 91
retry: 3000
data: heartbeat
data carries the payload, event names the event type (it defaults to a generic "message" if omitted), id is remembered by the browser as the Last-Event-ID for reconnection, and retry sets how long the browser waits before reconnecting after a drop.
A minimal client and server
// server.js (Express)
app.get("/api/stream/prices", (req, res) => {
res.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
res.flushHeaders();
const id = setInterval(() => {
const price = getLatestPrice("ACME");
res.write(`event: price_update\ndata: ${JSON.stringify(price)}\n\n`);
}, 2000);
req.on("close", () => clearInterval(id));
});
// client
const stream = new EventSource("/api/stream/prices");
stream.addEventListener("price_update", (e) => {
updateTicker(JSON.parse(e.data));
});
stream.onerror = () => console.log("Reconnecting automatically...");
No library is required on either side. The browser handles parsing, reconnection, and event dispatch on its own.
SSE versus the alternatives
| Approach |
Server push |
Reconnect handling |
Complexity |
| Polling (setInterval + fetch) |
No, client asks repeatedly |
Not applicable |
Low, but wasteful |
| Long polling |
Simulated |
Manual |
Medium |
| SSE (EventSource) |
Yes, native |
Automatic, with Last-Event-ID |
Low |
| WebSockets |
Yes, bidirectional |
Manual |
Medium to high |
When SSE fits and when it does not
SSE is a solid default for live notifications, activity feeds, AI-generated streaming text, score or price tickers, build and deploy logs, and any one-way dashboard. It is the wrong choice when the client also needs to send frequent messages back over the same channel — that is what WebSockets are built for. For the full bidirectional-versus-one-way decision, including how authentication and debugging differ between the two, see WebSockets vs server-sent events.
Common mistakes and limitations
Forgetting Cache-Control: no-cache. Without it, some proxies buffer the response and delay every message, which defeats the point of a live stream.
Trying to send binary data. SSE is a text protocol end to end. Binary payloads have to be base64-encoded first, which adds overhead — for real binary streaming, look elsewhere.
Assuming EventSource can set custom headers. It cannot. Most implementations authenticate through cookies sent automatically with the request, or a token in the URL query string, since a bearer-token header is not an option with the native API.
Skipping the id field when you actually need resumability. Without an id on each message, a reconnect after a drop starts fresh instead of picking up where the client left off.
FAQ
Can SSE send binary data?
Not directly. SSE is a text protocol, so every message is UTF-8 text. Binary payloads must be base64-encoded first, which adds overhead, making SSE a poor fit for large binary transfers.
Can the client send data back over an SSE connection?
No. SSE is one-way, server to client only. The client sends any input through a normal separate HTTP request, not through the EventSource connection itself.
How does authentication work with SSE?
The native EventSource API cannot set custom request headers, so most implementations authenticate through cookies or a token in the URL. A fetch-based streaming client is the usual workaround when a bearer token in a header is required.
What happens if the SSE connection drops?
The browser reconnects automatically, roughly three seconds later by default unless the server sent a different retry value. If the server included id fields, the browser resends the last one as a Last-Event-ID header so the server can resume from that point.
Where to go next