SSE for a real-time sensor dashboard
In one of my small sensor dashboard projects, I had multiple devices sending measurements to a backend, and a browser UI showing those measurements live.
At first, I was considering the usual options: polling or maybe WebSockets. Polling would work, but it means the browser asks the server for updates on a timer. WebSockets would also work, but they felt heavier than what I actually needed. The browser did not need a full two-way channel. It mostly just needed to receive updates whenever any device sent a new measurement. That made Server-Sent Events (SSE) a good fit.
SSE is simpler than it sounds. It is still plain HTTP over TCP. The browser sends a standard HTTP GET request that explicitly asks for an event stream, typically with Accept: text/event-stream. The server replies with Content-Type: text/event-stream. Then, instead of sending a normal finite response body and finishing, it keeps that same response open and writes new event data into it over time.
So SSE is not “many responses”. It is one HTTP request and one long-lived HTTP response whose body is streamed incrementally.
From JavaScript, it looks like this:
const es = new EventSource("/events");
es.addEventListener("measurement", (event) => {
const data = JSON.parse(event.data);
console.log(data);
});
Underneath, the request is still just HTTP:
GET /events HTTP/1.1
Host: example.com
Accept: text/event-stream
Cache-Control: no-cache
and the server replies with headers like:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Cache-Control: no-cache is common here because an SSE endpoint is a live stream. You do not want the browser, a proxy, or some intermediary treating it like a normal cacheable response and serving stale data.
The important part is what happens next. In a normal HTTP response, the server sends the body, finishes it, and the response is done. With SSE, the body does not finish. The server keeps the response open and keeps writing events into it over time.
A typical event might look like this:
id: 42
event: measurement
data: {"device":"sensor-03","value":19.2}
Each event ends with a blank line. That blank line is how the browser knows one event is complete.
At this point, it helps to distinguish SSE from ordinary persistent HTTP. Ordinary web traffic already keeps TCP connections open quite often. A browser may open one TCP connection and reuse it for several separate HTTP request-response exchanges: first the HTML page, then CSS, then JavaScript, then images, then maybe an API call. Each response still completes normally. The connection just stays open so later requests can reuse it.
So in ordinary persistent HTTP, the picture is roughly:
one TCP connection
GET /index.html -> response finishes
GET /styles.css -> response finishes
GET /app.js -> response finishes
GET /logo.png -> response finishes
GET /api/data -> response finishes
SSE is different. The browser sends one GET /events request, and the server returns one HTTP response that stays open while new events are appended to its body over time. So the key difference is not simply that the TCP connection remains open, because ordinary HTTP already does that. The difference is that this one response remains in progress and is treated as an event stream via Content-Type: text/event-stream.
At the transport level, nothing special changes: it is still HTTP over TCP. The browser keeps reading bytes from the same socket as the server writes more data. Since TCP is only a byte stream and does not preserve message boundaries, SSE defines its own boundaries in the text format: each event ends with a blank line. The format itself is minimal, using fields such as data:, event:, id:, and retry:. A single event can also contain multiple data: lines, which the browser joins with newline characters.
In my dashboard, the flow was simply:
device -> backend via POST
backend -> browser via SSE
The devices sent measurements to the backend using ordinary HTTP POST requests. The dashboard connected to /events, and whenever the backend received a new measurement, it pushed an SSE event to every connected dashboard page.
A practical detail is that long-lived HTTP streams can be closed by browsers, proxies, or load balancers if they sit idle for too long. That is why SSE endpoints often send heartbeat comments such as:
: heartbeat
The browser ignores those comments, but intermediaries still see traffic on the connection.
SSE also handles reconnects well. If the connection drops, the browser usually reconnects automatically. If events include IDs, the browser can send Last-Event-ID on reconnect so the server can resume from the last seen event, assuming it has enough history.
What I like about SSE is that it stays simple. It is plain HTTP, one long-lived response, and a small text-based event format. No protocol upgrade, no full-duplex complexity, no client polling loop.
For a dashboard showing measurements from multiple devices, that is a clean fit.