Server-sent events
Server-sent events is one HTTP response that never ends. The client makes an ordinary request, the server holds it open and writes text/event-stream records as things happen, and the browser reassembles them into events. It goes one way only — server to client — and it reconnects by itself. Connect's app uses it for its own event stream.
What goes down the wire#
GET /api/events HTTP/1.1
Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
event: thread.updated
id: 8412
data: {"thread_id":"th_example","state":"needs_you"}
: a comment line keeps the connection warm
event: call.ended
id: 8413
data: {"call_id":"cl_example"}Three things in that transcript do the real work. Each record ends with a blank line, which is how the client knows an event is complete. The id field is remembered, and on reconnection the client sends it back as Last-Event-ID so the server can resume rather than restart. And a comment line — a line beginning with a colon — carries no event but keeps intermediaries from deciding an idle connection is a dead one.
How it differs from WebSocket#
| Server-sent events | WebSocket | |
|---|---|---|
| Direction | Server to client only | Both ways |
| Protocol | Ordinary HTTP; no upgrade, no second scheme | An upgrade to ws:/wss:, a different protocol after the handshake |
| Reconnection | Built in, with resume from the last event id | Yours to write, including the resume logic |
| Payload | Text records | Text or binary frames |
| Infrastructure | Passes proxies and CDNs as a normal response | Needs upgrade support along the whole path |
| Cost of a mistake | A dropped stream reconnects | A stalled socket can look alive indefinitely |
The asymmetry is the feature, not a limitation to work around. An application whose client sends actions as ordinary requests and receives notifications as a stream has one authorisation path — every state change goes through the same middleware, the same allowlist and the same workspace filtering as any other request. A duplex socket invites a second path, and a second path is a second place for a permission check to be forgotten.
Does Connect use server-sent events?#
Used, for the application's own event stream. When a conversation changes, an approval appears in Needs You, or a call finishes, the browser learns about it over this stream rather than by polling. Connect does not use WebSocket for its own application data — that is a deliberate choice and not an accident of history.
WebSocket does appear in the product, in one unrelated place: SIP signalling for the browser softphone runs over it, because that is what the SIP-in-a-browser standard requires. Two different problems, two different transports; neither is a fallback for the other.
Operating one#
- Send heartbeats. An idle connection is indistinguishable from a broken one to a proxy, and many will close it. A comment line every few seconds costs nothing and prevents a class of phantom disconnection.
- Number your events. Without an
id, a reconnection restarts and the client silently misses whatever happened during the gap — the most common way a live screen ends up quietly stale. - Treat an event as a nudge, not as the truth. The safe pattern is *something changed, go and read it*, because that re-reads through the normal authorised path. An event carrying a full record has to carry its own authorisation story.
- Watch the per-origin connection budget. Over HTTP/1.1, browsers allow only a handful of connections to one origin, and a stream holds one for its whole life. Under HTTP/2 the limit is far higher; do not assume either.
Questions#
Why not just poll every few seconds?
Polling costs a full request round trip per interval per open tab, and it is still late by up to one interval. A held-open response costs one connection and delivers when the thing happens. Polling remains the right answer for a back-end integration that runs occasionally — that is a different question, answered separately for developers.
Can the client send anything back on the stream?
No. That is what an ordinary request is for. If you find yourself wanting a return channel, what you usually want is a normal API call plus the confirmation arriving as the next event.
What happens on a flaky mobile connection?
The browser reconnects on its own and replays the last event identifier it saw. Whether that resumes cleanly depends on the server honouring it — which is the argument for numbering events even when everything looks fine on a desk.