Skip to content

SSE: pauseOnBackground silently drops events with no resync, so backgrounded tabs go stale #1976

@davidpoblador

Description

@davidpoblador

Summary

Live views wired with hx-sse:connect + broadcast() silently go stale whenever the tab is backgrounded, with no built-in recovery. The combination of two framework behaviors causes events to be lost permanently:

  1. htmx 4's SSE extension sets pauseOnBackground: true for hx-sse:connect. When the tab is hidden it calls connection.reader.cancel(), which closes the stream and unsubscribes server-side.
  2. sse_endpoint(...) has no replay by default. Without buffer_size, _dispatch_local assigns no event id, broadcasts carry no id: field, and on reconnect there is nothing to replay. htmx reconnects but never re-fetches, so any broadcast() sent during the hidden window is lost forever.

Net effect: trigger an action, glance at another tab while the worker finishes, come back — the view is stuck on the old state until a manual reload. It is most visible with fast transitions (a sub-second status flip almost always lands during a navigation gap or glance-away), and invisible in local dev where the CSP is report-only and tabs stay focused during testing.

Reproduction

  1. A page with <div hx-sse:connect="…/events"><div hx-get="…" hx-trigger="my-event from:#stream">…</div></div>.
  2. Open it; let the SSE connect.
  3. Background the tab (or simulate: Object.defineProperty(document,'hidden',{get:()=>true}); document.dispatchEvent(new Event('visibilitychange'))).
  4. From another process, broadcast(channel, event="my-event", data="1").
  5. Foreground the tab. htmx reconnects, but the consumer never re-fetches — the view stays stale. A plain EventSource opened alongside (no pause logic) does receive the event, confirming the broadcast was delivered and only the paused htmx connection missed it.

Why it is a footgun

The documented pattern (hx-sse:connect + a hx-get consumer triggered by a named event) looks correct and works in every focused-tab test, but quietly loses events the moment attention shifts. Nothing in the SSE docs flags that backgrounding drops events or that recovery requires opting into a replay buffer / reconnect re-fetch.

Suggestions (any one would help)

  • Docs: note that pauseOnBackground drops events and recommend a resync-on-reconnect pattern, e.g. add htmx:after:sse:connection from:#stream to the consumer's hx-trigger so it re-fetches current state on every (re)connection.
  • Consider making buffer_size (Last-Event-ID replay) easier to reason about across multiple frontend workers — per-process monotonic ids mean a reconnect to a different worker can't replay reliably, so the buffer isn't a robust multi-worker answer today.
  • Optionally expose/recommend a knob to disable pauseOnBackground for views that must stay live.

Workaround we shipped

Added htmx:after:sse:connection from:#stream alongside the named event in the consumer's hx-trigger, so the view re-fetches current state on reconnect and recovers missed events (one idempotent fetch on initial connect). Confirmed htmx:before:sse:connection / htmx:after:sse:connection fire on the connecting element on every reconnect.

Environment: vibetuner 10.22.3, htmx.org 4.0.0-beta4.

Filed by Claude Code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions