Server-Sent Events: An Event Protocol over HTTP Streaming
- date
- category
- Frontend
- also in
- Backend · Networking
- reading
- 2 min / 410 words
Raw HTTP streaming gives us:
response body -> bytes over time
But bytes do not carry event semantics yet.
Server-Sent Events adds a standard format and a browser API.
The event stream
The server answers:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
and leaves the response open.
The data has simple framing:
id: 1843
event: order-status
data: {"id":42,"status":"completed"}
id: 1844
event: notification
data: {"message":"ready"}
An empty line ends an event.
The browser can use EventSource:
const source = new EventSource("/events");
source.addEventListener(
"order-status",
event => {
const order = JSON.parse(event.data);
render(order);
}
);
SSE is one-way
The communication model:
client -> HTTP request
server -> event
server -> event
server -> event
Application data flows from the server to the client.
If the client has to send something, it makes an ordinary request:
POST /messages
GET /events <- SSE stream
That is not a flaw if the domain naturally has that direction.
For example:
client -> start report generation
server -> progress 10%
server -> progress 40%
server -> progress 100%
We do not need a full-duplex protocol just because the data arrives "live".
Reconnection is part of the mechanism
EventSource automatically tries to reconnect after losing the connection.
The server can send:
retry: 3000
to suggest the reconnection delay in milliseconds.
Even more important is:
id: 1844
The browser remembers the ID of the last event.
On reconnect it can pass:
Last-Event-ID: 1844
The backend can resume the stream from the next event.
That does not automatically give exactly-once delivery.
It does give a standard mechanism for implementing resume.
The event ID forces a question about history
If the client comes back with:
Last-Event-ID: 1844
the backend has to answer a question:
can I still reproduce the events from 1845 on?
Possible models:
in-memory buffer
database/event log
message broker retention
no replay support
SSE solves the transport of events to the browser.
It does not solve the durability of events in the system.
Connection limits and the HTTP version
With HTTP/1.1 browsers have a low limit of parallel connections per origin, which can be a problem with many tabs and many SSE streams.
With HTTP/2 many application streams can share one HTTP/2 connection, and the limits are negotiated differently.
That is a good example of why:
application stream
!=
transport connection
A proxy can break "realtime"
The backend may flush an event immediately, but a reverse proxy may buffer the response.
The architecture then looks correct:
backend -> event -> proxy -> browser
while the proxy holds the data until it collects a bigger buffer.
So with streaming it is worth checking:
- proxy buffering,
- compression buffering,
- idle timeout,
- connection draining,
- CDN behavior.
Realtime is a property of the whole path, not of the endpoint alone.
SSE or WebSocket?
SSE is a very good choice when:
dominant direction = server -> client
For example:
- notifications,
- build/deploy status,
- observability feeds,
- report progress,
- AI output,
- live dashboards.
But there are systems where both sides send messages often and independently:
client -> cursor position
server -> remote cursor position
client -> game input
server -> world state
Two separate HTTP channels start to feel artificial there.
We need a persistent full-duplex channel.
That is WebSocket.