The expensive part of a live poll is usually delivery to watchers, not accepting the vote. With V votes, W connected watchers, and an average of B delivered bytes per update, publishing each vote creates up to V × W deliveries and roughly V × W × B outgoing bytes. Publishing one tally every interval instead changes the delivery term to I × W, where I is the number of intervals that actually produce an update. These are capacity-planning formulas, not measured benchmarks. The trade: viewers lose the individual vote stream, while the authoritative vote record remains available to the system that counts it.
Short answer: persist each accepted vote once, compute a versioned tally, and send the latest snapshot or coalesced tally update to viewers. Do not apply that same policy blindly to collaborative cursors. A cursor's latest position can replace its previous position, but its owner and expiry must remain unambiguous or the editor will display people who have already left.
What actually dominates the bill?
Imagine a developer-tools team running a poll inside an editor session. If 400 voters each cast one ballot while 20,000 clients watch, per-vote broadcasting asks the delivery layer to make 8,000,000 deliveries. One update per second for a 40-second voting window has at most 800,000 viewer deliveries when all intervals change the tally. The example assumes watchers remain connected and ignores framing, retries, compression, and transport overhead. It demonstrates a tenfold difference in delivery count for those inputs, not a promised reduction on a real network.
The storage bill is a separate decision. A vote ledger may be needed to reject duplicates, resolve disputes, or recount; a viewer does not need a durable copy of every intermediate screen state. Retain the ledger according to the poll's requirements and retention policy. Retain only a current tally plus a bounded recovery window for fanout if a disconnected viewer can reload an authoritative snapshot. Otherwise, shortening the delivery log can turn a reconnect into a silent gap.
That gap matters.
The analogy to OTP delivery is useful but limited: an accepted operation and a successfully displayed result are different events. An acknowledgement for a ballot should mean the counting system accepted it, not that every observer rendered it. Exposing that distinction helps with retries and with honest operational metrics.
When is a tally safer than every vote?
When viewers only need current totals, a tally is a sufficient view; a raw vote stream exposes more event detail and magnifies fanout. The server still needs a rule for concurrent updates. Assign each accepted tally a monotonically increasing revision within one poll, and publish a snapshot carrying the poll identifier, revision, counts, and server timestamp. On reconnect, fetch the latest snapshot before applying later updates. If revision 47 arrives after revision 48, ignore 47. If the viewer detects a gap and updates are deltas rather than full snapshots, reload instead of guessing the missing counts.
Aggregation has a limitation: it is unsuitable when the viewer must inspect each ballot as it arrives, such as an authorized audit console. In that case, deliver the individual events to the authorized audience and budget for its smaller, explicit fanout, while keeping public viewers on snapshots. If a screen must show an exact arrival order across ballots and presence changes, separate revision counters won't establish that order either; define a shared ordering domain or avoid promising one. This distinction is also a privacy decision: a viewer-facing stream of individual votes could reveal behavior that a tally does not, even when the payload omits names.
For a cursor, use a different state key: session and participant, with a sequence number and a server-side lease expiry. Keeping the latest cursor coordinate per participant is sensible; summing coordinates is not. A stale cursor is a presence error, not a harmless delay. Clients that resume after a network break should get a fresh presence snapshot, and departures should clear or expire the old entry even when no explicit leave message arrives. This is the accuracy boundary that prevents a poll optimization from becoming an editor bug.
Expiry is nonnegotiable.
Here is a small, transport-independent view update. The data store must assign revisions atomically; the function only guards the viewer against out-of-order snapshots.
def apply_tally(current, incoming):
if incoming["poll_id"] != current["poll_id"]:
return current
if incoming["revision"] <= current["revision"]:
return current
return incoming
An equal revision is a duplicate, so dropping it is intentional. A higher revision is safe here because the message contains absolute counts. A delta protocol needs gap detection and replay or a new snapshot.
How should recovery and testing work?
Start the accounting at the edge: measure accepted votes, published snapshots, connected watchers, outbound bytes, publish-to-render latency, and snapshot reloads separately. Compare event fanout against the interval policy using the same simulated vote distribution and watcher count. Bursty arrival matters. A quiet poll produces fewer changed intervals than a sustained one, while reconnect storms can make snapshot reads the dominant term after broadcast volume falls.
Test duplicate ballots, simultaneous ballots, a publish failure after a successful write, reversed snapshot arrival, and a reconnect across a retention cutoff. The invariant is that counting is independent of broadcast success. When the publish step fails, the next snapshot must still reflect the accepted ballot. Test presence with delayed cursor messages and an abrupt client disconnect; the old participant must eventually disappear according to the declared lease. Do not interpret a WebSocket connection alone as proof that a participant is still editing.
There is also a policy question: individual votes may carry identifiers or other sensitive data. Sending only aggregate counts to watchers reduces that exposure, but it does not remove the need to govern the retained ballot ledger. Choose its expiry from audit, privacy, and abuse requirements, then document who can access it. Avoid treating the fanout log as a substitute for an authoritative record.
What do we stop keeping?
Stop keeping per-viewer copies of every vote and, where snapshot reload is available, stop retaining a long history of intermediate tally broadcasts. Keep the accepted ballot record for its defined purpose, the authoritative current tally, and enough information to establish a new viewer's revision. For presence, keep only the latest sequenced state per active participant and a bounded expiry; do not preserve stale cursor positions as if they were current.
The cost is diagnostic detail. After an incident, a snapshot-only delivery layer cannot reconstruct the exact sequence each viewer saw. If that matters, keep sampled delivery traces or a short, access-controlled event window and state its retention period explicitly. No amount of cheaper fanout compensates for an unexplained wrong count or a cursor that never leaves.
Keep that boundary visible.
Further reading
- WebSocket Protocol, RFC 6455: https://www.rfc-editor.org/rfc/rfc6455
- HTTP Semantics, RFC 9110: https://www.rfc-editor.org/rfc/rfc9110
- W3C WebRTC 1.0: https://www.w3.org/TR/webrtc/
- OWASP WebSocket Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/WebSocket_Security_Cheat_Sheet.html
Top comments (0)