Konrad Kowalski (rootsher)Principal Platform & Reliability Architect100101100110000110110010011011100010110111110111

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:

text
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
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache

and leaves the response open.

The data has simple framing:

text
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:

js
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:

text
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:

text
POST /messages
GET  /events   <- SSE stream

That is not a flaw if the domain naturally has that direction.

For example:

text
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:

text
retry: 3000

to suggest the reconnection delay in milliseconds.

Even more important is:

text
id: 1844

The browser remembers the ID of the last event.

On reconnect it can pass:

http
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:

text
Last-Event-ID: 1844

the backend has to answer a question:

can I still reproduce the events from 1845 on?

Possible models:

text
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:

text
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:

text
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:

text
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:

text
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.