Server-Sent Events (SSE) is a standard HTTP response that just… never ends. The server keeps writing data: lines; the browser's EventSource API reads them. It's the simplest way to push from server to client, it survives corporate proxies in ways WebSockets don't, and it's what powers the streaming output of every modern AI chat product.
EventSource in every browser.Content-Type: text/event-stream that doesn't end. No protocol upgrade, no special framing.data: {"foo":1}, separated by a blank line.Last-Event-ID so the server can resume.The server writes lines; the client reads lines. No framing protocol. No Upgrade handshake. No subprotocol negotiation. A working SSE server is ten lines of Express, FastAPI, or Spring code.
id: 42
event: message
data: {"role":"assistant","content":"Hello"}
id: 43
data: {"role":"assistant","content":"!"}
It's an ordinary HTTP response. Every proxy, every CDN, every corporate firewall already understands it. Compare with WebSockets, where one corporate proxy that doesn't pass Upgrade headers cleanly takes down the whole feature.
The browser handles reconnect automatically. The server's optional id: field becomes a resume cursor — on reconnect the browser sends Last-Event-ID, and the server can pick up where it left off. None of this requires application-level code.
Standard cookies, bearer tokens, and CORS preflights work — it's just HTTP. Compare with WebSockets, where the browser's API blocks custom headers and you end up with query-string tokens or short-lived tickets.
The server can push but the client can't push back over the same connection. Use a regular POST for client-to-server messages. For most use cases this is a non-issue (and arguably cleaner — separate the concerns), but if you really need symmetric push, WebSockets win.
SSE is a text protocol. Binary payloads need base64 — fine for small things, wasteful for large ones. If you're streaming raw audio or video frames, look elsewhere (WebSockets, WebRTC, gRPC streaming).
Browsers limit connections per host (typically 6 on HTTP/1.1). Each SSE stream burns one of those slots. Open six streams and other requests stall. Mitigation: use HTTP/2 (no per-host limit), or fan multiple logical streams through one SSE connection with an event channel.
Long-lived connections are still long-lived. Per-connection memory, file descriptors, and worker slots add up. Plan capacity by concurrent connections, not by RPS. Use async runtimes (Node, Go, async Python, Spring WebFlux) — thread-per-connection servers fall over fast.
NGINX with the wrong config will buffer the entire response and defeat streaming. Set X-Accel-Buffering: no, ensure proxy_buffering off for the location, and disable response compression on the stream (gzip off for SSE responses).
graphql-sse) — an alternative to WebSockets for real-time GraphQL.| Need | Pick |
|---|---|
| Server pushes occasional updates; client uses normal REST to act | SSE. Simpler than WebSockets and just as effective. |
| Both sides push frequently and tiny payloads | WebSockets. Lower per-message overhead, true full-duplex. |
| Pushes are infrequent (minutes apart) and clients can tolerate delay | Polling or long-polling. Don't over-engineer. |
| Streaming AI completions | SSE. Industry default for a reason. |
| Binary streams (audio, video) | WebSockets, WebRTC, or HLS, not SSE. |