Lesson 0004 · ~12 minutes

When “easy client” still fails

One skill: spot production constraints that kill native EventSource — or force HTTP/2, cookies, or fetch streaming — before you green-light the design.

Win for this lesson

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.

1. Client is easy; the edges are not

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.

2. Auth & request shape

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
Design review question

“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.

3. CORS

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.

4. Proxies, buffering, idle kills

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.

5. Connection budget (the multi-tab footgun)

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.

Mitigations

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.

Escape hatches

WebSockets use a different pool (higher limits in practice). Or accept polling for low-frequency tabs. Or collapse to one multiplexed event bus channel.

6. Risk scan (steal this)

60-second SSE risk scan
  1. Direction still one-way? (Else WebSocket.)
  2. Auth: cookies OK, or need Bearer/POST? (Else fetch stream.)
  3. Cross-origin? CORS + credentials planned?
  4. Proxy path: buffering off, timeouts, heartbeats?
  5. HTTP/1 or h2? How many tabs × streams per origin?
  6. Resume: gap OK or Last-Event-ID + log required?

7. Practice — call the risk

Case A

API gateway requires Authorization: Bearer on every request. SPA has no cookie session for APIs. Best client approach?

Case B

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?

Case C

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?

Case D

SPA on app.example.com, stream on api.example.com, session cookie auth. Stream fails CORS. Correct fix direction?

Case E

LLM UI must POST a large prompt JSON, then read token events. Product wants SSE-shaped events. Client choice?

8. What to remember

Ask your teacher Your real stack’s auth, CDN, or “we already use GraphQL subscriptions” — bring it. Deal-breakers are where wisdom from your environment matters most.

Primary source

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).