Module 05 · Communication patterns · Lesson 2 of 10
Server-sent events
A one-way stream of events over a plain HTTP response. The wire format, automatic reconnection with Last-Event-ID, where SSE beats WebSockets for server-to-client updates, and the proxy and connection-limit pitfalls.
Why it matters
Most "real-time" features are one-directional: the server has news, the client displays it. Notifications, live dashboards, progress bars, streamed AI responses. WebSockets handle this but bring a second protocol with its own infrastructure needs. Server-sent events do the one-way case with nothing but HTTP, and most teams that reach for WebSockets first would have been better served by them.
The idea
Server-sent events (SSE) is an HTTP response that never ends. The server replies with Content-Type: text/event-stream and writes events as they happen; the browser's built-in EventSource API parses them and fires callbacks. The connection stays open as long as both sides want it.
The wire format is plain text, one event per blank-line-separated block:
id: 1042
event: order.updated
data: {"id": 9001, "status": "shipped"}
: heartbeat comment, ignored by clients
id: 1043
data: plain text works too
Built-in reconnection
When the connection drops, EventSource waits (a server-settable retry: interval) and reconnects on its own, sending a Last-Event-ID header with the last id it received. A server that keeps a short buffer of recent events per stream can replay what the client missed, so a brief network blip loses nothing and the application code never sees it. This is the feature WebSocket users end up hand-building.
SSE versus WebSockets
| Server-sent events | WebSockets | |
|---|---|---|
| Direction | Server to client only | Both directions |
| Protocol | Plain HTTP; works through any HTTP proxy, CDN and load balancer that supports streaming | Upgrade to a separate framed protocol; needs explicit support on every hop |
| Reconnect and resume | Built in, with Last-Event-ID | Build it yourself |
| Payload | UTF-8 text (JSON works fine) | Text or binary frames |
| Browser connection limit | 6 per origin on HTTP/1.1, effectively unlimited on HTTP/2 | Separate limit, usually hundreds |
| Authentication | Cookies or a query token; no custom headers from EventSource | Cookies or a token in the first message or query |
| Fits | Feeds, notifications, progress, streamed text | Chat, games, collaborative editing, anything the client also sends frequently |
The client can still send things to the server: it uses ordinary HTTP requests for that. The question is whether client-to-server messages are frequent and latency-sensitive enough to need a persistent channel of their own. If not, SSE plus regular POSTs is simpler and more robust.
Pitfalls
Buffering proxies and some application servers hold the response until it is "complete", which never happens; disable response buffering (nginx X-Accel-Buffering: no) and compression on the stream route. Send a comment line every 15 to 30 seconds as a heartbeat so idle timeouts do not close the connection and clients can detect a dead one. On HTTP/1.1, browsers allow six connections per origin, so several tabs each holding a stream exhaust the budget; HTTP/2 multiplexes them over one connection and removes the problem. Every open stream is a held connection on the server, so use an event-driven runtime and plan fan-out: a pub/sub layer delivers an event once to each server, which writes it to its local streams.
Trade-offs
SSE is simple, standards-based, proxy-friendly and self-healing, at the cost of being one-way and text-only, and of needing a persistent connection per client just like WebSockets. For server-to-client updates it is almost always the lighter choice; the moment the client needs a low-latency channel upstream, WebSockets earn their complexity.
In an interview
Choose it deliberately: "Order status updates are server-to-client only, so SSE: one GET /events stream per session, ids on every event, a short replay buffer so Last-Event-ID resumes after a blip, heartbeats every 20 seconds, and a Redis pub/sub channel fanning events out to whichever server holds the client's stream. Client actions are ordinary POSTs." Then pre-empt the question: "WebSockets only if we add a feature where the client sends frequently, like chat."
Check yourself
A dashboard needs live metrics pushed to the browser every second, and the browser only ever sends an occasional filter change. Which transport fits best and why?