Lesson 0004 · ~12 minutes
One skill: spot production constraints that kill native
EventSource — or force HTTP/2, cookies, or
fetch streaming — before you green-light the design.
You run a 60-second risk scan: auth shape, CORS, proxy buffering, connection budget — and pick EventSource vs fetch-stream vs WebSocket with eyes open.
You already know the happy path is a few lines of
EventSource. Teams still burn weeks on four edge classes.
None of them invalidate SSE as a pattern — they change
how you ship it.
The constructor is essentially
new EventSource(url, { withCredentials? }). There is no
headers map. Spec discussion
WHATWG #2177
treats custom headers as out of scope for the built-in API: if you need
them, build on fetch().
| Your auth model | Native EventSource? | Typical move |
|---|---|---|
| Cookie session (same origin) | Yes | Default path |
| Cookie / credentials cross-origin | Maybe |
withCredentials: true + CORS credentials headers
|
Authorization: Bearer … required |
No (not via API) |
fetch stream + parse SSE, or cookie for stream, or
careful token-in-URL (last resort)
|
| POST body to start stream | No | fetch with method POST + body stream |
“How does the stream authenticate?” If the answer is only “same Bearer header as our REST clients,” budget a non-EventSource client or change the stream’s auth model.
EventSource is subject to the same cross-origin rules as other network
APIs
(web.dev).
Cross-origin streams need the usual CORS allowlist. With credentials,
the server cannot use Access-Control-Allow-Origin: * — it
must echo a specific origin and allow credentials.
The wire is a long-lived response. Anything that buffers the full body
before flush will make “live” events arrive in late batches — or never
until disconnect. Common nginx pattern: turn off response buffering for
that location and/or send
X-Accel-Buffering: no from the app; raise proxy read
timeouts. WHATWG
authoring notes
also recommend periodic : comment lines so legacy proxies
do not drop idle connections.
FE symptom to remember: DevTools shows the request “pending” forever,
no error, no events → often buffering or missing
text/event-stream, not a bad onmessage
handler.
On HTTP/1.1, browsers commonly limit about six concurrent connections per origin. Each open SSE holds one. Open the app in several tabs, or open several streams per page, and other API calls queue behind them. Eric Lawrence (text/plain) documents this starvation; the WHATWG authoring notes warn about the same class of limit.
Serve SSE over HTTP/2+ (multiplexed streams on one connection). One shared EventSource for the app (not one per widget). SharedWorker fan-out if you must share across tabs. Avoid “stream per component” architecture on h1.
WebSockets use a different pool (higher limits in practice). Or accept polling for low-frequency tabs. Or collapse to one multiplexed event bus channel.
API gateway requires Authorization: Bearer on every
request. SPA has no cookie session for APIs. Best client approach?
App opens one SSE per page. Support reports: with 4–5 tabs open on HTTP/1.1, REST calls hang until a tab closes. Root cause class?
Server logs show events written every second. Browser shows the SSE request pending; UI updates only after a long delay or disconnect. First place to look?
SPA on app.example.com, stream on
api.example.com, session cookie auth. Stream fails
CORS. Correct fix direction?
LLM UI must POST a large prompt JSON, then read token events. Product wants SSE-shaped events. Client choice?
Eric Lawrence — The Pitfalls of EventSource over HTTP/1.1 for the connection-limit mental model. Pair with WHATWG authoring notes (proxies, heartbeats, connection limits) and issue #2177 (headers / fetch).