# 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.

- **Status:** Reference
- **Audience:** developer, both
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/technology/sse/

## What goes down the wire

```http
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.

> **Note** Events are described for machines in an AsyncAPI **3.1.0** document, alongside the OpenAPI description of the request surface. If you are writing a client, read that rather than reverse-engineering the stream.

## 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.

## Related

- [WebSocket](https://connectbyjbrh.com/docs/technology/websocket/)
- [HTTP](https://connectbyjbrh.com/docs/technology/http/)
- [SIP over WebSocket](https://connectbyjbrh.com/docs/technology/sip-over-websocket/)
- [The AsyncAPI description](https://connectbyjbrh.com/developers/asyncapi-events/)
- [Webhooks or polling](https://connectbyjbrh.com/developers/webhooks-vs-polling/)
- [What Connect may do](https://connectbyjbrh.com/docs/autonomy/)

## What this page is based on

- Connect capability registry (docs-source/facts.py) — PROTOCOLS.asyncapi, CHANNELS.softphone
- Connect source pack (docs-source/sources/GENERAL.md §2, §5) — request flow and Needs You
- https://html.spec.whatwg.org/multipage/server-sent-events.html — Server-sent events
