Skip to content

feat(sse): harden SSE and replace polling with push - #362

Merged
cebarks merged 4 commits into
mainfrom
sse-hardening
Jul 30, 2026
Merged

feat(sse): harden SSE and replace polling with push#362
cebarks merged 4 commits into
mainfrom
sse-hardening

Conversation

@cebarks

@cebarks cebarks commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add 30-second keepalive heartbeat to the SSE events stream to prevent proxies from closing idle connections
  • Cap concurrent SSE connections at 128 with a 429 response when exceeded, preventing resource exhaustion from runaway tabs
  • Replace aggressive polling (1-15s intervals) with SSE push triggers on dashboard, headless, and integrity progress templates — each uses its matching sse:* event with a long (30-60s) fallback poll for resilience
  • Remove redundant hand-rolled EventSource script from headless page (the HTMX SSE extension in base.html handles it natively)

Kept polling where it's the right pattern: operation tracking (transient element, no per-op event), stash (external profile data, no event), and proxy metrics (high-frequency counters).

Test plan

  • Verify build, tests, and clippy pass (all clean)
  • Verify SSE keepalive prevents idle connection drops behind reverse proxies
  • Verify 129th concurrent SSE connection gets 429
  • Verify dashboard panels refresh instantly on server state changes, mod operations, player events, and headless changes
  • Verify headless page refreshes on headless state changes without the old EventSource script
  • Verify integrity progress updates on integrityChanged events
  • Verify polling fallbacks still work if SSE connection drops

Implemented with the help of Claude Code

cebarks and others added 4 commits July 29, 2026 19:21
Rewrites the main SSE events endpoint to use actix_web_lab::sse::Sse
with with_keep_alive(30s), matching the pattern already used by the log
stream handlers. Prevents proxies and load balancers from closing idle
SSE connections.
Adds an AtomicUsize counter to AppState that tracks active SSE event
stream connections. New connections are rejected with 429 when at the
cap. A drop guard ensures the counter decrements when the stream task
ends (client disconnect or channel close). Prevents runaway browser
tabs from exhausting server resources.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dashboard, headless, and integrity progress templates were polling the
server at 1-15s intervals even though the SSE infrastructure already
broadcasts events for the exact state changes they care about. Switch
each to use its matching sse:* trigger with a long (30-60s) fallback
poll for resilience.

Removes the redundant hand-rolled EventSource script in headless.html
since the HTMX SSE extension (wired globally in base.html) now handles
it natively via the sse:headlessChanged trigger.

Kept polling where it's the right pattern: operation tracking (transient
element, no per-op event), stash (external profile data, no event), and
proxy metrics (high-frequency counters, pushing would be worse).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cebarks
cebarks enabled auto-merge (squash) July 30, 2026 01:22
@cebarks
cebarks merged commit 87531e9 into main Jul 30, 2026
11 checks passed
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