Termio
Blog

web / 2026-05-12 / 7 min read

Why browsers cap HTTP/1.1 SSE connections and block your POSTs

Learn how too many EventSource streams can exhaust the browser's HTTP/1.1 connection budget and make POST requests hang.

A send button that spins forever is not always a backend bug. If the request never appears in server logs, the browser may be waiting for a connection slot. This happens easily when a page opens many Server-Sent Events streams over HTTP/1.1.

The browser budget

HTTP/1.1 clients limit concurrent connections per origin. The common browser budget is small enough that six long-lived connections can starve the rest of the page. EventSource uses an HTTP request that stays open, so it consumes one of those slots for as long as the stream is active.

The dangerous pattern is one stream per item. A dashboard with twenty panes, cards, jobs, or devices can open twenty streams to the same origin. The first few connect. The rest sit pending. A laterfetch() for a button click queues behind them and appears to hang.

// Avoid: connection count grows with rows on the screen.
rows.map(row => new EventSource(`/api/rows/${row.id}/events`));

How to diagnose it

Open DevTools, go to Network, filter by event-stream, and count pending requests to the same host. Then click the broken button. If the POST does not leave the browser, the server cannot log it. That absence is the clue.

Invert the stream

The fix is to make connection count independent of UI item count. Use one SSE stream that multiplexes events for all rows, one WebSocket, or one polling endpoint that returns the current state map.

// Better: one connection carries updates for all rows.
const events = new EventSource("/api/dashboard/events");
events.onmessage = (event) => {
  const update = JSON.parse(event.data);
  applyUpdate(update.id, update);
};

Polling is not a failure. For small status payloads, a five-second poll can be more reliable and simpler than dozens of long-lived streams. The right question is not "streaming or polling?" It is "does the number of open connections scale with the number of visible entities?"

HTTP/2 changes the limit, not the design rule

HTTP/2 multiplexes many streams over one connection, so the exact symptom may disappear when the full path from browser to app is HTTP/2. But products move through proxies, staging stacks, and local dev servers. Designing one stream per entity still creates avoidable load and harder failure modes.

Treat long-lived browser connections as a scarce resource. Spend one per feature area, not one per row.