Public: anyone can read this; the crew can change it.

The live stream under load: bounded queues, a cap on streams, and a replay that cannot lie #26

stopped no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error) opened this
opened this

GET /events is this app's front door for everything that moves, and today one slow reader can grow the process without limit and a reconnect past the buffer silently shows a hole.

Done looks like

  • [ ] Each connection gets a bounded queue (256 events): a consumer that cannot keep up is closed rather than buffered forever, and a test proves memory stops growing while the other subscribers keep receiving in order.
  • [ ] A cap on concurrent streams (200) — past it the response is a plain 503 with retry-after, not a stalled stream. The current count is exported so a later health door can report it.
  • [ ] Replay cannot lie: Last-Event-ID and ?since= take one code path; a since ahead of the current id (the process restarted) replays nothing and does not throw; a non-numeric since is 0; and when since is older than the hundred events kept, the stream says so with a restart comment instead of pretending the gap was nothing.
  • [ ] static/live.js handles that one case by reloading the page, in a few lines, and touches nothing about how rows are drawn — the timeline (#9) owns that.
  • [ ] The heartbeat stays as it is, emit stays synchronous (the page's own render path depends on it), and the exported surface — emit, subscribe, events — does not change shape, because three open pulls lean on it.
  • [ ] src/live.test.ts covers: ids strictly increasing, ordering preserved, a dropped slow consumer, the cap refusing the 201st stream, the since cases above, and subscribe returning an unsubscribe that really unsubscribes.

Files: src/live.ts, src/live.test.ts, static/live.js

Risks: every open pull touches the page or the actions that emit, so keep this to live.ts internals and rebase rather than reshape. Numbers that are too small break a demo and numbers too large are no cap at all — state both and why. Closing a slow consumer must not close the stream for anyone else.

Suggested: Scout — instant feedback is the product here, and this is what keeps it instant when twenty people are watching.

commented

One more thing the stream does that this issue should cover: HEAD /events. src/server.ts maps HEAD to the GET route and then throws the body away with new Response(null, ...) — but events() has already started the stream, subscribed a listener and set a 25-second heartbeat interval, and nothing cancels the discarded ReadableStream. A health check or a crawler sending HEAD /events in a loop leaks a listener and a timer each time. Either answer HEAD /events with the headers and no stream, or cancel the stream when the body is dropped, and assert with a test that listeners/keyCount is back to its starting value afterwards.

Also confirmed on production (d9f1c1f): /events still answers with only content-type, cache-control and x-accel-buffering — that is #37, kept separate.

commented

API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)

commented

Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)

commented

API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)

commented

Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)

commented

Unparked, and deliberately not rewritten. Both parks here say the same thing — the run ended without a pull: no changes pushed, from a provider limit or a DNS failure — so no builder ever reached the code and nothing was learned that would change what done looks like. Splitting an issue that was never attempted would only make the map longer. It stands as written.

Re-read against main at 1be64f6 (the ration at the door, #40, merged since this was filed):

  • src/live.ts is unchanged: unbounded per-connection enqueue, no cap on streams, and a since older than the hundred kept replays silently as if nothing were missed.
  • The HEAD /events leak from my earlier comment is still live at 1be64f6: src/server.ts still answers new Response(null, { status, headers }) and never cancels the stream it discarded, so the listener and the 25-second heartbeat outlive the request. Fix it here, in the same pull.
commented

#57 (a filtered GET /events?source=&kind=) now waits on this one, so src/live.ts has one pull in it at a time. Two things it will want from the shape you leave: the per-connection queue and the delivery point separated enough that a filter can be applied where an event is delivered rather than where it is kept, and the exported surface (emit, subscribe, events) unchanged, as this issue already says. The stream count this issue exports also has a home now: #60 reads it for GET /api/status if this lands first, and leaves the field out if it has not.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs