Treat realtime presence membership as a recent observation, never as proof that a participant saw, processed, or answered a live poll. Explained in operational terms, presence can tell you which identities have represented connections; it cannot tell you who received the question. For a logistics session, keep poll delivery and vote acceptance in a durable event path, attach explicit identifiers, and use presence only to improve the interface.
TL;DR: a driver appearing in a session roster means the presence system currently associates that identity with one or more live connections. It cannot establish that the poll reached the driver's screen, that the screen was visible, that the driver was able to respond, or that a missing connection represents an absent person. Delivery guarantees come from acknowledgements and persisted state, not membership.
That distinction matters during a dispatch session asking, "Can you accept the revised loading window?" A tempting dashboard says 38 online, 38 sent, and therefore 38 delivered. Only the first claim follows from presence, and even that claim depends on heartbeat timing, connection identity, and the semantics chosen for multiple devices.
What can realtime presence membership tell a poll operator?
Presence usually starts with a lease-like observation: a client joins a named session, periodically renews its connection state, and eventually leaves or expires. The roster is a projection of those observations. This is realtime membership explained without the marketing gloss: useful, but not a census.
Consider dispatcher d-17, warehouse lead w-04, and driver r-208. If r-208 has a phone and a cab tablet connected, a connection-level roster contains four connections for three people. If the phone enters a tunnel, the server may continue showing its lease until the expiry window passes. If the tablet reconnects before the earlier connection expires, the roster can temporarily contain both generations. None of those states is evidence of a defect. They are consequences of observing distributed clients across time.
The identity key changes the meaning of the number. Counting socket IDs answers "how many connections are represented?" Counting stable user IDs answers "how many identities have at least one represented connection?" Counting session roles answers another question again. Pick one, name it in metrics and UI, and resist calling all three "users online."
That is the boundary.
Presence reports system-observed membership, not human attention. A connected browser may be backgrounded. A foreground tab may be covered. A driver may have handed the device to a colleague. Web APIs can expose some local page state, but a server-side membership record cannot infer those facts by itself. In concrete examples, an "online" cab tablet can be locked, and a missing phone can still belong to a driver watching the dispatcher on a shared warehouse display.
Build the poll around receipts, not the roster
The data flow is small enough to state plainly. The poll service persists a poll with a stable ID and monotonically increasing revision, publishes a lightweight notification to the session, accepts idempotent votes, and records acknowledgements separately. Presence supplies a current audience estimate for the operator screen. It does not decide whether a vote is valid and does not become the system of record.
Here is a compact TypeScript model. The interfaces are deliberately transport-neutral, so WebSocket, server-sent event, or data-channel wiring can sit behind them without changing the delivery contract.
type Poll = {
id: string;
sessionId: string;
revision: number;
closesAt: string;
question: string;
options: readonly string[];
};
type PollNotice = {
pollId: string;
revision: number;
};
type Vote = {
pollId: string;
voterId: string;
option: string;
idempotencyKey: string;
observedRevision: number;
};
interface PollStore {
get(pollId: string): Promise<Poll | undefined>;
recordVote(vote: Vote): Promise<"accepted" | "duplicate" | "closed" | "stale">;
}
interface SessionBus {
publish(sessionId: string, notice: PollNotice): Promise<void>;
}
async function announcePoll(poll: Poll, bus: SessionBus): Promise<void> {
await bus.publish(poll.sessionId, {
pollId: poll.id,
revision: poll.revision,
});
}
async function submitVote(vote: Vote, store: PollStore): Promise<string> {
const poll = await store.get(vote.pollId);
if (!poll) return "unknown poll";
if (vote.observedRevision !== poll.revision) return "refresh required";
return store.recordVote(vote);
}
The notice is a hint to fetch or reveal the persisted poll. A reconnecting client can recover it without replaying an ephemeral presence transition. The idempotencyKey makes a retried submission distinguishable from a second intent, while observedRevision prevents a response to an old loading-window proposal from silently applying to its replacement. This concrete split is what realtime presence cannot tell you: membership describes a connection projection, while a receipt describes one application event.
Notice what the example does not do: it does not snapshot the roster and reject votes from identities missing from that snapshot. Membership can change between the snapshot, notification, render, and click. Authorization should come from the session's access rules and poll eligibility data, both evaluated when the vote is accepted.
Where does the delivery guarantee begin?
It begins only when the application defines an event and records evidence for it. "Published" can mean the server accepted a notification. "Received" can mean client code acknowledged a specific poll revision. "Rendered" requires a client-side observation tied to the visible UI. "Answered" means the vote store accepted an idempotent command. These are different milestones.
Use a narrow state model instead of upgrading one signal into another:
| State | Evidence | What it still cannot prove |
|---|---|---|
| Represented | Unexpired membership for an identity | Device reachability now, attention, or delivery |
| Notified | Fan-out system accepted the poll notice | Any particular client received it |
| Received | Client acknowledged poll ID and revision | The poll was visible or understood |
| Displayed | Client reported the active view | A human read it |
| Answered | Durable vote was accepted | Who physically operated the device |
Three numbers beat one vague label.
This costs a few extra records. It also prevents a much more expensive argument after a loading-window decision: the operator can distinguish 31 represented identities, 29 acknowledgements, and 27 accepted votes rather than presenting a single inflated "online" count.
Be conservative with displayed receipts. The Page Visibility API exposes whether a document is hidden, and its state can help the client decide when to report a display event. It still does not measure attention. Browser throttling and network loss can delay that report, so product copy should say "display reported" rather than "seen."
Fast fan-out and durable recovery also solve different problems. A low-latency channel gets the notice onto active connections. A persisted poll plus a cursor or revision lets a reconnecting client discover what it missed. If the business requirement says every eligible participant must have a chance to answer, build recovery against the eligibility set. Repeatedly broadcasting to the current presence roster is not equivalent.
The failures hiding inside a clean member count
Disconnects are ambiguous. A member can disappear because the app closed, a radio changed networks, a heartbeat was delayed, the server restarted, or the lease expired. Presence cannot choose among those explanations. Labeling every departure as "left the session" invents user intent.
Duplicates are equally ordinary. One person may use two devices; one browser may briefly own an old and a new connection during recovery. Deduplicate by stable identity for the human-facing count, retain connection IDs for diagnostics, and include a connection generation when stale leave events could remove a newer join. The exact expiry interval is a product decision: a longer lease reduces visible flapping but extends stale membership; a shorter one detects loss sooner but reacts more strongly to jitter.
Ordering deserves its own check. Suppose r-208 reconnects and join generation 42 arrives before the delayed leave for generation 41. A projection keyed only by user ID may process the leave last and incorrectly show the driver offline. Comparing generations, or applying server-assigned sequence numbers within a documented scope, prevents that stale event from winning. This is also why a clean realtime membership list cannot tell an operator that its latest state reflects causal order; the projection must enforce that rule.
No global clock saves this. WebSocket defines a full-duplex connection, but an application still has to specify reconnection, acknowledgement, identity, and persistence semantics. WebRTC data channels can carry arbitrary data between peers and support ordered or unordered delivery configurations; they do not turn a peer-presence observation into proof of application-level receipt. Transport capability is not business evidence.
Backpressure is the less glamorous failure. If 2,000 session connections all receive a poll at once, a slow consumer can accumulate queued notices while still looking present. Set payload limits, bound per-connection queues, expose queue age, and disconnect or degrade clients under an explicit policy. Then let recovery fetch the active poll. Keeping an overloaded connection in a roster protects the wrong metric. It may make the membership chart look calm while the delivery path is late, which is precisely the distinction the operator needs the system to expose.
Calm charts can lie.
Operate the meaning, not just the channel
Before release, test the semantic edges with fake time and controlled reordering: two connections for one identity, acknowledgement loss followed by retry, expiry during a poll, a late leave from an earlier generation, and a vote submitted against a replaced revision. Load tests should report fan-out acceptance separately from end-to-end acknowledgement latency. Otherwise a fast publish call can hide a slow or blocked audience.
In production, graph represented identities, active connections, notification attempts, acknowledgements by poll revision, accepted votes, duplicate submissions, stale revisions, lease expirations, and queue age. Avoid putting raw user IDs into high-cardinality metric labels; keep traceable identifiers in access-controlled logs instead. Alert on ratios and age distributions that correspond to the contract, not on a cosmetic online total.
The operational checklist is mostly language discipline. Write down whether membership is keyed by connection or identity, how long an observation remains valid, what reconnect does, and which service owns poll truth. Make the UI use the same terms. Confirm that deployments preserve or rebuild projections from authoritative events, while active polls remain recoverable from durable storage. Finally, rehearse a partial fan-out and verify that acknowledgements expose the gap without blocking legitimate late votes.
Ship the smaller claim. Presence is excellent for hints such as "participants are available" and for reducing pointless sends to obviously empty sessions. It is a poor foundation for compliance, attendance, or guaranteed delivery. For a live logistics poll, the defensible number is the count backed by the milestone you actually recorded.
References
- WebSocket Protocol, RFC 6455: https://www.rfc-editor.org/rfc/rfc6455
- W3C WebRTC 1.0: https://www.w3.org/TR/webrtc/
- W3C WebRTC Data Channels: https://www.w3.org/TR/webrtc/#datachannel
- MDN Page Visibility API: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
- MDN WebSocket API, including the backpressure limitation: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
Top comments (0)