Skip to content

SSE single-poller fan-out: bound Redis load to O(queues), not O(connections)#13

Merged
StrangeNoob merged 8 commits into
mainfrom
sse-fanout
Jun 9, 2026
Merged

SSE single-poller fan-out: bound Redis load to O(queues), not O(connections)#13
StrangeNoob merged 8 commits into
mainfrom
sse-fanout

Conversation

@StrangeNoob

Copy link
Copy Markdown
Owner

Summary

Replaces the per-connection SSE polling on GET /api/stream with a single in-process fan-out hub, so dashboard streaming scales with queue count instead of connection count.

  • Before: every open dashboard ran its own server-side goroutine polling Redis every ~1s (a SCAN for queue discovery + Stats/Counters per queue). Redis load was O(connections × queues)/sec, and every connection recomputed the identical global snapshot — the limiter well before Go's HTTP layer.
  • After: one lazy background poller per process reads Redis once per interval and broadcasts a single snapshot to all subscribers. Redis load is O(queues)/sec, independent of connection count; an idle server (no dashboards) does no polling.

This implements the fan-out that CLAUDE.md previously flagged as future work.

Design

  • New hub (internal/api/hub.go) owns all polling + broadcast, behind a snapshotSource interface (satisfied by *broker.Broker) so its fan-out/lifecycle is unit-tested without Redis.
  • Lazy lifecycle: poller starts on the first subscribe, stops on the last unsubscribe.
  • Latest-wins delivery: each subscriber has a cap-1 channel; a slow tab only ever holds the newest snapshot and never blocks the poller.
  • Instant populate preserved: the poller polls immediately on start, and late joiners are seeded from the cached last snapshot.
  • Wire format byte-identical (data: <json array>\n\n) — the committed web/dist dashboard is untouched; no frontend change, no dist rebuild.
  • Spec: docs/superpowers/specs/2026-06-09-relay-sse-single-poller-fanout-design.md.

Test Plan

  • go test -race ./... — all packages pass, including the real-Redis TestStreamEmitsSnapshot (runs through the hub, not skipped)
  • Six new hub unit tests (no Redis): lazy start/stop, fan-out to all, slow-consumer-doesn't-block + latest-wins, late-joiner cache, poll-error survival
  • go build ./... and go vet ./internal/api/ clean
  • web/ untouched; only CLAUDE.md + 4 internal/api files changed
  • Spec-compliance, code-quality, and final holistic reviews all passed

Replaces per-connection Redis polling (O(connections x queues)/sec) with one
in-process hub: a single background poller broadcasts snapshots to all
subscribers, bounding Redis load to O(queues)/sec. Lazy lifecycle (polls only
while >=1 dashboard connected), wire-format unchanged, no new deps.
@StrangeNoob

Copy link
Copy Markdown
Owner Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@StrangeNoob StrangeNoob merged commit 4abfa84 into main Jun 9, 2026
4 checks passed
@StrangeNoob StrangeNoob deleted the sse-fanout branch June 9, 2026 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant