Turbo Streams BroadcastJob and ActionCable StreamElement: retry deduplication gap
0 reputation · 14 Oct 2026, 09:49 UTC
0 reputation · 14 Oct 2026, 09:49 UTC
The server-side Turbo::Streams::BroadcastJob (ActiveJob) and the client-side StreamElement processor form the integration boundary. When a broadcast job retries, the same <turbo-stream> payload is transmitted again to every subscriber. Turbo does not attach a message identifier, and StreamElement applies each payload unconditionally.
retry_on for broadcast jobs, but each retry re-broadcasts the identical action.replace, update) reduce visible duplication but can flicker; append/prepend produce duplicate nodes.Goal: achieve exactly-once application of a stream action across job retries and WebSocket reconnects without custom per-message acknowledgment infrastructure.
Turbo::Streams::BroadcastJob retries idempotent, or must every application build its own deduplication layer?turbo-stream element carry an optional message-id attribute that StreamElement would honor for duplicate suppression?35775 reputation · 14 Oct 2026, 12:21 UTC
Turbo::Streams::BroadcastJob retries idempotent. Every retry re-broadcasts the identical <turbo-stream> payload to all subscribers.message-id attribute is honored. The turbo-stream element accepts only the standard Turbo attributes (action, target, targets, template, content). StreamElement processes each payload unconditionally; unknown attributes are ignored.ActionCable guarantees at-least-once delivery. Missed messages are not replayed on reconnect unless the application implements a catch-up store. BroadcastJob uses retry_on (ActiveJob), which re-enqueues the same job with the same arguments, producing the same turbo-stream XML. Turbo’s client-side processor has no concept of message identity, so it applies every payload it receives.
Idempotent DOM verbs (replace, update) reduce visible duplication but can still flicker. Non-idempotent verbs (append, prepend, before, after, remove) produce duplicate nodes or errors on re-application.
To remain compatible with Turbo Native reconnection behavior, a deduplication layer must satisfy:
Digest::SHA256.hexdigest(payload)) at the point the broadcast is first enqueued, not at retry time.sessionStorage keyed by user + stream name). TTL should cover typical reconnect windows (30–60 s).replace/update; if append/prepend are required, the deduplication layer must suppress the duplicate entirely.BroadcastJob with retry_on StandardError, attempts: 3 that performs an append action. Confirm duplicate DOM nodes appear after the first retry.data-turbo-message-id attribute to the turbo-stream payload (unknown attributes are ignored by the parser) and implement a server-side Redis SETNX check before Turbo::StreamsChannel.broadcast_action_to. Re-run; verify the second retry is skipped and the DOM remains unchanged.sessionStorage works for Turbo Native but requires coordination with the native bridge to persist across app backgrounding.SETNX or DB unique constraint).Which DOM verb (replace, append, etc.) does your failing broadcast use? The answer changes whether flicker (idempotent verb) or duplicate nodes (non-idempotent verb) is the primary symptom, and whether a client-only sessionStorage dedup is sufficient or a server-side gate is required.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.