A shared channel makes a live support poll easier to operate, but a disconnected device needs a way to recover the events it missed. A channel per device narrows delivery and authorization scope, yet it does not solve recovery by itself. Short answer: choose the channel layout for fan-out and access boundaries; put reconnect and backfill on a durable, ordered event log with a cursor. Treat the device's online status as a separate observation, not proof that its vote arrived.
Should one channel serve all devices or a channel per device?
Imagine an agent asking participants in an active support session whether the proposed fix worked. A laptop and a phone can belong to the same participant. Each may reconnect after the poll opens; either can show an old device-status badge while the other has already submitted a vote. If the UI subscribes again and waits only for new messages, it may miss the opening question or show a stale tally. Rejoining is not backfill.
Give every committed poll event a stable identifier and a sequence within the session. The client persists its last applied cursor, not merely the last packet received. On reconnect it requests events after that cursor, applies them in sequence, and then follows the live stream. The server must define where replay meets live delivery so an event committed during the handoff cannot disappear between the two. Duplicates can happen at that boundary; idempotent application makes them harmless. A poll close event belongs in the same ordering domain as votes and tally updates so the client cannot apply a late tally as though the poll remained open. For example, if the phone last applied sequence 41 and a laptop submitted a vote at 42 while the phone was offline, the phone must receive 42 even if it reconnects after the live subscriber has seen 43. These numbers illustrate an ordering case, not a measured workload.
No cursor, no recovery.
The cursor belongs to an ordered stream, not to a socket. If you partition the stream by device, a device-specific offset cannot by itself recover session-wide poll events from another partition. Conversely, a shared session cursor should not grant access to every private device-status event. This distinction matters whenever a participant switches from phone to laptop mid-session.
Derive the channel boundary from authorization
For a poll whose question, close state, and aggregate tally are visible to everyone in the support session, a session-scoped shared channel fits the visibility boundary. Publish a device's private connectivity or delivery state only to an authorized device or participant scope. A per-device channel for every poll update multiplies subscriptions and fan-out work as devices accumulate; it can still be appropriate when payloads truly differ by device. The important question is which principals may see an event, not how many sockets can receive it.
Voting is a command, not a broadcast. Accept it through an authenticated write path with a stable idempotency key, decide whether a participant may vote once or revise a vote, and commit the resulting event before reporting success. This echoes an OTP delivery rule: an acknowledgement that a request was accepted is not evidence that every endpoint displayed it. Likewise, an online badge should not be used as a vote receipt. The sender can lose its connection after the write commits but before it receives an acknowledgement; retrying with the same key should return the established outcome rather than count another vote.
The following is an illustrative replay filter, not a complete authorization or transaction implementation. The session and principal checks must happen before querying, and a database transaction must establish the committed ordering represented by seq.
def events_after(events, session_id, cursor, permitted_types):
return [
event for event in events
if event["session_id"] == session_id
and event["seq"] > cursor
and event["type"] in permitted_types
]
A session-wide sequence can be assigned at commit under a per-session serialization rule; do not assume a database-generated number alone implies commit order. If transactions allocate numbers and commit out of order, a client that advances past a late commit can miss it permanently. That is an ordering bug, not a transport bug.
Compare the operational consequences
| Boundary | Shared session channel | Channel per device |
|---|---|---|
| Poll fan-out | One session publication can reach eligible listeners | The same public update must be addressed to each device |
| Access control | Every published event must be safe for the entire authorized session | Private device payloads have a narrower audience |
| Reconnect | Replay session events after a session cursor | Replay needs a device cursor plus a source for shared poll state |
| Offline device | Other listeners continue; device catches up later | Its private queue or replay window needs explicit retention policy |
These are design consequences, not performance benchmarks. An implementation may batch fan-out, use a broker, or materialize a poll snapshot; actual costs depend on subscriber count, event frequency, retention, and how authorization is checked. Measure those on the expected workload. Avoid treating per-device isolation as a substitute for access checks: channel membership must still be authenticated and revoked when session membership changes. The limitation of a shared channel is that it cannot safely carry private device payloads to every session member; the limitation of a per-device channel is the extra subscription and delivery work for identical public poll updates. Choose the boundary based on who may read each event.
There is no universal winner.
Transport semantics are another independent choice. WebSocket provides a bidirectional connection, while server-sent events are a one-way stream and define a Last-Event-ID reconnect mechanism. Neither protocol creates a durable event log automatically. WebRTC data channels have their own transport behavior and do not turn a missed application event into a recoverable database record. Pick a transport that matches the write and read paths, then test recovery at the application layer.
Roll out recovery without guessing
Start by recording session event IDs, accepted vote IDs, and the cursor acknowledged as applied by each client. Backfill a bounded gap after reconnect; if the cursor has fallen outside retention, return a current poll snapshot plus a new cursor and clearly replace stale local state. Close the poll against the authoritative server state, never against a device clock. Define what a client sees when its permission is revoked during replay, and keep private device status out of a session-wide snapshot.
Then test four timing cases: disconnect just before a vote commits, disconnect after commit but before acknowledgement, reconnect while the poll closes, and retry the same vote from two devices. Track reconnect gaps, replay lag, duplicate suppression, rejected writes, and snapshot fallbacks separately. That split tells an operator whether a missing result is a delayed UI, an authorization rejection, or a write that never committed.
Deploy the replay path before switching clients to cursor-based recovery, and retain the previous state view until older clients have aged out. A modest poll does not need an elaborate channel tree. It does need an unambiguous answer to which events each device is allowed to recover after it comes back.
Sources
References:
Top comments (0)