DEV Community

XenonCross2718
XenonCross2718

Posted on

Fintech Poll Message Size for Small SaaS — Publish Per Viewer or Whole Queue

Short answer: publish one aggregate poll update to everyone authorized for the session, and send participant-specific state only to that participant. If the alternative is one private copy per participant, compare total delivered bytes, not the size of a single publish call. Never put a whole list of voters into a broadly subscribed message merely to reduce publish count. This decision holds even if the aggregate payload is a little larger than a single private update: token scope determines who may see the data; message size determines whether the resulting fan-out is affordable and responsive.

Should a small SaaS publish per viewer or send a whole queue message?

Consider a live poll during a fintech education session. A participant can see the current choice totals and whether their own vote was accepted. The first field is session-wide; the second is private. A signed-in session viewer is not automatically entitled to another viewer's vote. The same discipline applies to an OTP status notification: broadcasting a personal acknowledgment because it happens to share a channel with a public counter would cross a trust boundary.

For this decision record, the invariants are explicit. A server validates the participant's identity and session eligibility before accepting a vote. The server computes aggregate totals; clients do not send authoritative totals. A session subscription receives only aggregate counts and a monotonically increasing revision. A participant-specific acknowledgment goes to a separately authorized scope. The browser is an untrusted subscriber even when its token is valid. Token claims limit its reach; they do not make fields safe to reveal to everyone holding the same session token.

This is a publication decision, not a vote-storage design. The durable vote record and the live notification have different jobs. A disconnected client must be able to recover the latest aggregate from an authenticated read instead of treating a missed live event as the permanent truth. In particular, a delivery queue containing pending acknowledgments is an internal transport detail, not a data model suitable for broadcasting. If its entries include participant IDs, sending the whole queue turns a retry mechanism into a disclosure mechanism. If the queue is drained slowly, its oldest entries may already be stale when a new viewer connects. Use an authorized state read for recovery, and treat each queued publication as an individually scoped delivery.

Scope first.

Where does the byte budget go?

The useful comparison is delivery volume across all recipients. Let N be the number of connected session viewers, A the serialized aggregate payload size, P the serialized private acknowledgment size, and Q the size of a full roster-shaped snapshot. A shared aggregate delivered to N viewers transfers approximately N × A application-payload bytes. Sending a private acknowledgment to each of N participants transfers N × P; sending a roster snapshot to everyone transfers N × Q. These are planning estimates, not wire measurements: framing, retransmission, transport metadata, compression, and reconnect behavior change actual traffic.

Publication shape Data in each delivery Approximate payload delivered to N viewers Permission boundary
Aggregate event to session Counts and revision N × A Every authorized session viewer
Private event per participant Own acceptance state N × P if all N vote One identified participant
Full roster to session Each person's state N × Q Exposes participant-level data to the session

For a concrete sizing exercise, suppose N = 200, an encoded aggregate is 120 bytes, a private acknowledgment is 90 bytes, and a roster snapshot is 8,000 bytes. A single aggregate delivery round accounts for about 24,000 payload bytes; a round of 200 private acknowledgments accounts for about 18,000; the roster accounts for about 1,600,000. Those are illustrative input sizes, not a benchmark or a promise about any transport. Measure serialized UTF-8 payloads from your own schema and include the number of events per vote: publishing a fresh aggregate on every accepted vote produces more deliveries than sending one after a bounded coalescing interval.

Count the amplification twice: once for egress, then for client work. Parsing and rendering a roster on every update can dominate a small screen's workload even when the server's publish API accepts the payload. A rate limit on voting does not solve subscription fan-out; those are different traffic paths. The aggregate approach has a limitation too: if every vote triggers a publication to every connected viewer, it can produce substantial delivery traffic during a burst. A bounded coalescing interval reduces publications but delays the displayed tally; choose that interval against the session's actual freshness requirement, then measure it.

No free broadcast.

How should the critical path enforce scope?

The following Python sketch keeps the vote write and its publication separate. store and publisher are application-defined interfaces, not claims about a particular SDK. The store must return an authoritative, committed revision and totals; it must also make repeated submission IDs idempotent so a retried request cannot add a second vote. Authorization precedes both the write and the private publish.

def submit_vote(store, publisher, identity, session_id, option_id, submission_id):
    participant_id = identity.participant_id
    if not store.can_vote(participant_id, session_id):
        raise PermissionError("not eligible for this session")

    result = store.record_vote_once(
        session_id=session_id,
        participant_id=participant_id,
        option_id=option_id,
        submission_id=submission_id,
    )
    if result.new_revision:
        publisher.publish_session(
            session_id,
            {"revision": result.revision, "counts": result.counts},
        )
    publisher.publish_participant(
        participant_id,
        {"submission_id": submission_id, "accepted": result.accepted},
    )
    return result.accepted
Enter fullscreen mode Exit fullscreen mode

In production, the database commit and outbound publication need a defined failure boundary. A transactional outbox is one way to record publication intent with the vote, then dispatch it after commit; duplicate dispatch remains possible, so subscribers ignore aggregate revisions older than the latest applied revision. Avoid publishing an acceptance before the vote commits. Also avoid granting a token access to a private channel solely because the client supplies a participant ID: derive that scope from the authenticated identity on the server.

The example deliberately omits account identifiers and option labels from the shared event. A count can still be sensitive in a tiny session, so assess whether a session-wide count is appropriate for that audience before granting subscription access. Small groups change the privacy analysis.

Why reject the whole snapshot?

A complete per-participant snapshot is tempting because a reconnecting client can replace its local state in one step. In this poll, putting it on the shared channel fails the privacy invariant and makes each update grow with the number of participants. Shrinking field names or enabling compression does not change who can read the contents. Publishing the roster separately to every participant avoids the shared-channel disclosure but repeats the large payload N times.

The snapshot has a valid use case: an authenticated moderator view whose role permits inspection of individual responses. Serve that view through a role-scoped read, with pagination or a bounded result size when the roster grows. It should not inherit the general session viewer's subscription token. That distinction is easier to audit than a client-side rule to hide fields after receipt. Conversely, a small private poll with just one entitled viewer has little fan-out advantage from a shared aggregate channel; a direct, scoped state read may be simpler. That is a real limitation of the broadcast design, not a reason to expand the token's audience.

Test this boundary with two accounts and a moderator role. Subscribe with each account's token, attempt to request the other account's private scope, retry the same submission ID, then disconnect during publication and reconnect to fetch the current revision. Record encoded payload sizes and the 95th-percentile time from committed vote to visible aggregate under expected concurrency. Keep the last two measures separate: a fast acknowledgment does not prove that the shared tally is current. Deployment should include a way to observe outbox age, publish retries, denied subscriptions, and client revision gaps without logging vote choices or bearer tokens.

The decision is therefore narrow: broadcast a minimal aggregate only to the entitled session, route personal acceptance privately, and recover missed state from an authorized read. Revisit the byte estimates as audience and vote frequency change; never expand token scope to make the publish path look simpler.

References

Top comments (0)