Designing a Live Polling & Q&A System for Large-Scale Virtual Events
A production-grade walkthrough of real-time polling and Q&A for events with tens of thousands of concurrent attendees — ingest, sharded aggregation, push-based fan-out, reliability, and the interview-style deep dives that separate a good answer from a great one.
Introduction & History
Live polling and Q&A systems let a presenter ask a question to a large audience and see aggregated results appear on screen within a second or two, while attendees simultaneously submit and upvote questions for the speaker. What looks like a simple feature — a bar chart that updates itself — is actually a distributed systems problem that combines high-fan-in write traffic, high-fan-out read traffic, real-time aggregation, and strict ordering and consistency guarantees, all under a hard latency budget.
The category traces its roots to classroom “clicker” devices used in the 1990s and 2000s, where students pressed physical remotes to answer multiple-choice questions and a base station tallied the results. As internet connectivity became ubiquitous, this evolved into web and mobile based tools. Products like Poll Everywhere, Mentimeter, and Slido popularized browser-based live polling for conferences and classrooms in the 2010s. When the COVID-19 pandemic forced conferences and town halls to go fully virtual almost overnight, these tools had to scale from hundreds of participants in a room to tens of thousands of attendees spread across the globe, joining from browsers, mobile apps, and embedded webinar platforms simultaneously — a jump of two to three orders of magnitude in a very short time.
Classroom clickers
Students press physical remotes to answer multiple-choice questions; a base station tallies results in the room — the earliest ancestor of today’s live polling.
Browser-based polling
Poll Everywhere, Mentimeter, and Slido popularize web polling for conferences and classrooms — still mostly hundreds of participants per event.
Virtual-event scale
Pandemic-era virtual conferences push the same tools from hundreds to tens of thousands of concurrent attendees — a two-to-three-order-of-magnitude scaling jump.
That scaling jump exposed the real engineering challenge: it is easy to build a poll that works for 50 people using a single server and a SQL UPDATE ... SET count = count + 1 statement. It is extremely difficult to build one that works for 50,000 people voting within the same 10-second window, where every single vote should be reflected in an aggregated result broadcast to every attendee’s screen with sub-second latency, without melting your database.
Picture a stadium full of fans holding up colored cards during an interval quiz. A single scorekeeper trying to count every card individually would fall hopelessly behind. Instead, section captains tally their own block, then a central scoreboard sums the sections. That’s exactly how sharded aggregation works — and why a naive single-counter design collapses at scale.
- Do you understand why this is a “hard” system design problem rather than a CRUD app with a UI on top?
- Can you articulate the difference between a write-heavy fan-in problem (votes) and a read-heavy fan-out problem (broadcasting results) happening in the same system?
- Do you know where the industry’s mental model of “clicker to real-time distributed counters” comes from, showing you understand the problem’s evolution, not just today’s tools?
Problem Framing & Requirements
Before drawing boxes and arrows, a senior architect nails down what “large-scale virtual event” actually means numerically, because the design decisions for 500 concurrent users are entirely different from those for 50,000.
Functional Requirements
- Presenters can create polls (multiple choice, word cloud, rating scale, open text) tied to a live event/session.
- Attendees submit votes/answers in real time; each attendee can vote once per poll (or per allowed number of times, e.g. multi-select).
- Aggregated results (counts, percentages, word cloud weights) update on presenter and attendee screens in near real time.
- Attendees submit Q&A questions; other attendees upvote questions; the moderator/presenter sees the ranked question list live.
- Moderators can moderate content (hide/delete inappropriate questions, close polls).
- Historical results are available after the event for export/reporting.
Non-Functional Requirements
- Scale: 50,000+ concurrent attendees in a single session; bursts of tens of thousands of votes within a 5–10 second window when a poll opens.
- Latency: Aggregated result updates should reach clients within roughly 500ms–2s of a vote being cast (perceived as “instant” but not literally real time to the millisecond).
- Consistency: Exactly-once (or effectively-once) counting per attendee per poll — no double counting, no silently dropped votes; approximate/eventually-consistent aggregate display is acceptable, but the final tally after poll close must be exact.
- Availability: The system must not go down mid-event; events are scheduled, non-negotiable, and failures are highly visible (a stalled poll on a 20,000-person livestream is a very public incident).
- Fan-out efficiency: One aggregated update must reach tens of thousands of open connections without linear cost per attendee on the write path.
- Abuse resistance: Prevent bots/scripts from stuffing votes or spamming Q&A.
Separate the problem into three sub-problems that have different scaling shapes — (1) ingest (many small writes arriving concurrently), (2) aggregate (combining those writes into a compact summary), and (3) fan-out (delivering that summary to many readers cheaply). Most of the interesting design decisions live at the boundaries between these three stages.
Sizing the Problem in Numbers
System design interviews reward turning vague requirements into a rough numeric model early. A reasonable working set of assumptions for “a large virtual event”:
| Dimension | Assumption | Implication |
|---|---|---|
| Concurrent attendees | 50,000 | 50,000 concurrent WebSocket connections to maintain |
| Poll participation rate | ~70–80% of attendees vote on a given poll | ~35,000–40,000 votes per poll |
| Vote submission window | Most votes land within the first 10–15 seconds of a poll opening | Peak ingest rate can be 4,000–8,000+ votes/sec, not the smoothed average |
| Result update cadence | Every 500ms–1s while a poll is open | Fan-out layer must push ~1–2 broadcasts/sec × 50,000 connections |
| Q&A questions per session | Hundreds to low thousands, with upvotes numbering in the tens of thousands | Upvotes behave like a second, parallel high-fan-in stream needing the same treatment as votes |
| Event duration | 30 minutes to several hours | Connections must be held reliably for extended periods, including across brief network hiccups |
These numbers matter because they immediately rule out naive approaches: 8,000 writes/sec against a single relational row, or 50,000 clients polling an HTTP endpoint every second (which alone is 50,000 req/sec of pure waste), are both non-starters at this scale — which is exactly why the architecture below leans on stream processing and push-based fan-out from the outset rather than retrofitting them after a proof-of-concept falls over.
Architecture & Components
At a high level, the system separates the “hot path” (votes flowing in, results flowing out, both needing to be fast and horizontally scalable) from the “control path” (poll creation, moderation, historical reporting, which can tolerate normal request/response latency and standard CRUD patterns).
Component Breakdown
- CDN / Edge: Serves static assets (JS bundles, poll UI) close to attendees globally, reducing load-time latency and offloading the origin.
- Global Load Balancer: Routes attendees to the nearest healthy regional cluster; performs TLS termination and DDoS scrubbing.
- API Gateway (REST/HTTP): Handles stateless request/response operations — poll creation, fetching poll definitions, authentication, historical data.
- WebSocket Gateway Cluster: Holds the long-lived, persistent connections attendees use to receive real-time updates (or Server-Sent Events / long-polling as a fallback). This tier is deliberately stateless regarding business logic — it only manages connections and subscriptions to broadcast topics.
- Rate Limiter / Abuse Filter: Sits in front of ingest to reject duplicate votes, throttle per-connection request rates, and catch obvious bot patterns before they hit the streaming backbone.
- Vote / Q&A Ingest Services: Stateless services that validate a vote or question (Is the poll open? Has this attendee already voted? Is the attendee actually registered for this event?) and publish an event to the streaming backbone.
- Partitioned Message Broker (Kafka/Kinesis-style): The backbone that decouples ingest from aggregation. Votes for a given poll are partitioned (e.g., by
poll_id) so ordering per poll is preserved and aggregation workers can consume in parallel across partitions. - Stream Aggregation Workers: Consume the vote/question stream and maintain running aggregates (counts per option, top-N ranked questions) using approximate, mergeable data structures.
- In-Memory Aggregate Store (Redis Cluster): Holds the current “hot” aggregate for each active poll, using atomic counter/sorted-set operations for very low-latency reads and writes.
- Pub/Sub Broadcast Service: Takes a computed aggregate snapshot (or delta) and fans it out to every WebSocket gateway node subscribed to that poll’s topic, which then pushes to individual client connections.
- Durable Vote Log (Cassandra/DynamoDB-style wide-column store): The append-only source of truth for every individual vote/question — used for exact final tallies, audits, and replay/recovery.
- Metadata DB (PostgreSQL): Stores structured, low-volume, relational data — events, sessions, polls, users, moderation state.
- Analytics Store (ClickHouse/BigQuery): Receives the full event stream for post-event reporting, funnel analysis, and export — decoupled from the real-time path so heavy analytical queries never compete with live traffic.
- “Why not just write directly to the database and read from it for the live chart?” — Be ready to explain why a relational DB doing read-your-writes with tens of thousands of concurrent small transactions will choke, and why decoupling ingest (stream) from serving (in-memory aggregate) is the standard fix.
- “Why two separate connection types — HTTP for submitting a vote and WebSocket for receiving updates?” — Submitting a vote is a simple, idempotent, retryable request/response operation; receiving updates is a long-lived subscription. Conflating them into one channel couples very different reliability and scaling concerns.
- “What happens if the WebSocket gateway tier restarts?” — Clients reconnect and resubscribe; because the gateway tier is stateless (state lives in Redis/Pub-Sub), no aggregate data is lost, only a brief reconnect blip per affected client.
Internal Working
The key internal insight is that the system never tries to update “the chart” directly from a vote. Instead it treats voting as a stream-processing problem: individual events flow through a pipeline that continuously folds them into a small, cheap-to-broadcast summary.
Idempotency and Exactly-Once Semantics
Each vote carries a deterministic idempotency key, typically hash(attendee_id, poll_id) (or including an option_id for multi-select polls where an attendee is allowed several distinct picks). Before a vote is admitted, the ingest service checks a fast idempotency store (Redis SETNX or a similar atomic “set if not exists”) keyed on that identifier. If the key already exists, the vote is treated as a duplicate/resubmission (common with flaky mobile networks retrying a request) and acknowledged without being counted twice. This single check is what prevents the most common correctness bug in these systems: a user’s spotty connection causing their client to retry a vote submission, which without idempotency would silently double their vote’s weight in the final tally.
Mergeable Aggregates (CRDTs and Approximate Counters)
Because multiple aggregation workers process different partitions of the same poll’s vote stream in parallel, the aggregate data structure must be safely combinable regardless of the order updates arrive in. This is the classic use case for CRDTs (Conflict-free Replicated Data Types) — specifically G-Counters (grow-only counters) or PN-Counters for vote tallies per option, which guarantee that merging partial counts from different workers, in any order, any number of times, produces the same correct total. For Q&A upvote ranking, a common technique is a probabilistic top-K structure (or simply per-question atomic counters combined with a periodic re-sort) since exact real-time global ranking of tens of thousands of questions is unnecessary — only the top ~20 questions the moderator will actually look at matter for the live view.
- “Why not just use a single global counter with a mutex?” — At tens of thousands of votes per second across a fleet of workers, lock contention on a single counter becomes the bottleneck. Sharded counters (or CRDT-based merging) let increments happen in parallel and get combined cheaply.
- “How do you avoid double-counting if two aggregation workers somehow process the same vote?” — CRDTs and idempotent, deterministic merge functions make re-application of the same event a no-op (associative, commutative, idempotent — the “ACI” properties), so at-least-once delivery from the broker doesn’t turn into over-counting downstream.
Push vs. Pull for Result Delivery
Attendee clients do not poll an HTTP endpoint every second to check for new results (that pattern collapses under tens of thousands of concurrent polling clients). Instead, clients hold a persistent WebSocket (or SSE) connection and are pushed a compact delta or snapshot whenever the aggregation layer decides it’s time to publish — typically on a fixed cadence (e.g., every 500ms–1s) rather than on every single vote, which smooths out the fan-out load and avoids “flickering” numbers on screen.
Topic-Based Subscription Model
Each active poll maps to its own logical broadcast topic (e.g. event:{event_id}:poll:{poll_id}). A client subscribes to the topic(s) relevant to whatever it’s currently viewing and unsubscribes when it navigates away or the poll closes, so gateway nodes never waste bandwidth pushing updates for content a given client isn’t looking at. This topic-per-poll granularity is also what makes multiple simultaneous polls within one event (common in breakout sessions) tractable — each has an entirely independent aggregate, partition set, and broadcast topic, with no shared mutable state between them.
Delta vs. Full Snapshot Broadcasts
Two broadcast strategies are used depending on payload size: for a small, fixed poll (e.g., four multiple-choice options), it’s simplest and cheapest to just broadcast the full current tally every interval — the payload is tiny and clients need no reconciliation logic. For larger result sets (a Q&A ranked list of dozens of questions, or a word cloud with hundreds of distinct terms), sending only the delta since the last broadcast (which items moved, new items that entered the top-K) keeps payload size roughly constant regardless of how large the underlying dataset grows, at the cost of slightly more complex client-side state reconciliation and the need for periodic full-snapshot resyncs to correct any drift from a missed delta.
Data Flow & Lifecycle
Here is the end-to-end journey of a single vote — from the tap on an attendee’s phone all the way to every subscribed screen refreshing the chart.
Notice the client receives a fast acknowledgement (their vote was accepted) separately and much sooner than the aggregate update that reflects it on everyone’s chart. This separation is what allows the ingest path to stay low-latency (a few tens of milliseconds) while the fan-out path batches on a coarser cadence for efficiency — the two are deliberately decoupled.
Poll Lifecycle States
- Draft — created by presenter, not visible to attendees.
- Open — broadcast to attendees, ingest path accepts votes, aggregation workers actively publishing updates.
- Closed — ingest path rejects further votes (idempotently, so late/duplicate network retries are simply dropped); a final, exact tally is computed by reading the durable vote log directly (bypassing any approximate/streaming shortcuts) to guarantee the displayed final number is authoritative.
- Archived — results persisted for post-event reporting; hot data evicted from Redis to reclaim memory.
The live, in-flight aggregate shown while a poll is open is allowed to be eventually consistent (it may lag by a fraction of a second or momentarily miss a very recent vote), but the moment a poll is closed, the system switches to computing the final count from the durable, ordered vote log — trading a little latency for a hard consistency guarantee at exactly the moment people are looking closely at the number (“and the winner is…”).
Real-Time Aggregation Engine (Deep Dive)
This is usually the section that differentiates a strong system design answer from a mediocre one, because it’s where the actual “hard” engineering happens.
Why Sharded, Approximate Aggregation Beats a Single Source of Truth Counter
A single row in a relational database with vote_count = vote_count + 1, hit by 10,000 concurrent requests, serializes on row-level locks and becomes the system’s bottleneck almost immediately — you’d be lucky to sustain a few hundred updates per second before latency spikes. The fix is the same pattern used by high-traffic counters everywhere (think “like” counts on major social platforms): split the counter into N independent shards (e.g., 16 or 64 per poll option), let writers increment a random shard, and sum the shards only when a reader needs the total. Writes become embarrassingly parallel; reads pay a small, bounded summation cost.
Windowed vs. Cumulative Aggregation
For simple vote counts, aggregation is cumulative — every vote ever cast for an option contributes to its running total for the poll’s lifetime. For “trending questions” or momentum-based ranking in Q&A (upvotes gained in the last 2 minutes matter more than upvotes from 20 minutes ago), a sliding time-window aggregation (e.g., an exponentially decayed score, or a fixed tumbling-window count) is more appropriate, similar to how trending-topic systems weight recency.
Backpressure Handling
When a poll opens to 50,000 attendees simultaneously, the ingest rate can spike far above steady-state. The message broker’s partitioned, durable log acts as a shock absorber — the ingest tier can accept and durably queue votes faster than the aggregation tier consumes them, and consumers catch up at their own sustainable pace without data loss. This is the classic producer/consumer decoupling benefit of a log-based broker over a direct RPC call from ingest straight into an in-memory aggregator.
- “Walk me through what happens if the aggregation worker fleet falls behind during a traffic spike — do attendees see stale numbers?” — Yes, briefly; the broker buffers durably, so nothing is lost, but the “real-time” chart may lag by a couple of extra seconds until workers catch up. This is an intentional, acceptable trade-off (availability and durability over instant freshness).
- “How would you compute an exact final count if you were using approximate/probabilistic structures during the live phase?” — Recompute directly from the durable, append-only vote log at poll-close time, which is the authoritative source of truth; the streaming aggregate is only a fast approximation for the “live” experience.
- “How many shards per counter, and how did you pick that number?” — Enough shards that contention per shard stays below the point where lock/CAS retries dominate latency at your peak QPS per poll option; too many shards makes summation slower and wastes memory. This is a tunable, workload-driven trade-off, not a fixed constant.
Word Clouds and Open-Text Aggregation
Word-cloud style polls (attendees type free-text answers, and the most common words/phrases render larger) add a normalization step ahead of aggregation: incoming text is lowercased, trimmed, and mapped through a light stemming/synonym pass (so “Cloud,” “cloud,” and “clouds” collapse into one bucket) before being fed into a sharded frequency counter identical in spirit to the multiple-choice vote counters. Because the space of possible answers is unbounded (unlike a fixed set of poll options), the aggregation layer typically tracks only an approximate top-N frequent terms using a space-saving/count-min-sketch style structure rather than an exact count for every distinct word ever submitted, which keeps memory bounded even if attendees submit thousands of unique responses.
Consistency Model, End to End
It’s worth being explicit about exactly which consistency guarantee applies at each stage, since “eventually consistent” is often used too loosely in interviews:
| Stage | Guarantee | Rationale |
|---|---|---|
| Vote acceptance (client ack) | At-least-once durability once the broker accepts the publish | The client just needs to know its vote was received and will be counted; it doesn’t need to know the current total |
| Per-attendee vote uniqueness | Exactly-once, enforced via idempotency key | Correctness-critical — double counting one person’s vote is a real bug, not just a UX nit |
| Live aggregate shown while poll is open | Eventually consistent, typically within one broadcast interval (e.g., ≤1s) of the true state | Acceptable staleness in exchange for throughput and simplicity; humans don’t notice sub-second lag on a moving chart |
| Final tally at poll close | Strongly consistent, computed directly from the durable, ordered vote log | This is the number people screenshot, quote, and act on — it must be exact and reproducible |
This tiered approach — relaxed consistency where it’s cheap and unnoticed, strict consistency exactly where it’s visible and consequential — is a recurring theme in large-scale real-time systems and a strong signal in an interview that a candidate isn’t reaching for “just make everything strongly consistent” as a default.
Advantages, Disadvantages & Trade-offs
Every design choice above buys something and costs something. Naming both explicitly is what turns an interview answer from a laundry list into a defensible design.
Pros
- Horizontally scales to tens of thousands of concurrent attendees and bursty vote spikes without touching a single hot row.
- Ingest and fan-out are independently tunable — one bug or slowdown in aggregation cannot stop the system from accepting votes.
- Durable vote log means nothing is ever lost and final tallies are exact and auditable.
- Push-based fan-out over persistent connections is dramatically cheaper than 50k clients polling every second.
Cons
- Considerably more moving parts than a “just write to Postgres” MVP — broker, cache, pub/sub, gateway all need operating.
- Live aggregate is eventually consistent — explaining that to a nervous product manager “why isn’t the number exact right now?” is part of the job.
- Millions of long-lived sockets are a very different beast operationally from stateless HTTP — connection storms, reconnect logic, kernel tuning all become first-class concerns.
- Final-tally recompute from the log at poll close adds a small extra step at exactly the wrong moment (the presenter’s big reveal), so it has to be latency-budgeted for.
| Decision | Advantage | Trade-off / Cost |
|---|---|---|
| Stream-based ingest + async aggregation (vs. direct DB writes) | Handles massive write bursts without collapsing; horizontally scalable | Added architectural complexity; results are eventually consistent while a poll is open |
| Sharded/CRDT counters (vs. single locked counter) | Near-linear write scalability, no lock contention | Reading the “exact” total requires a summation step; slightly higher memory footprint |
| Push over persistent WebSocket (vs. client polling) | Dramatically lower server load and lower latency at scale | Requires managing millions of long-lived connections and their failure modes; more complex infra than plain HTTP |
| Periodic snapshot broadcast (vs. broadcasting every single vote) | Bounded, predictable fan-out bandwidth regardless of vote volume | Introduces up to one broadcast-interval of latency (e.g., 500ms) on updates |
| Durable vote log as source of truth (vs. trusting the in-memory aggregate) | Guarantees exact, auditable final results; enables replay and recovery | Extra storage cost and write path; final tally computation adds a bit of work at poll close |
Performance & Scalability
At the target scale (tens of thousands of concurrent attendees, bursts of tens of thousands of votes within seconds), performance work concentrates on three specific bottlenecks.
Connection Scale on the WebSocket Tier
A single modern server can typically hold on the order of tens of thousands to around a hundred thousand idle-ish WebSocket connections, depending on message frequency, heap/connection overhead, and OS tuning (file descriptor limits, ephemeral port ranges, kernel socket buffer tuning). For 50,000+ concurrent attendees, the gateway tier is horizontally scaled behind a load balancer that supports sticky/consistent routing, with connection state kept minimal (essentially just “which topics is this socket subscribed to”) so any gateway node is interchangeable and stateless from the app’s perspective.
Fan-out Cost Control
Broadcasting to 50,000 sockets from a naive single publisher is itself a scaling problem. The standard fix is a pub/sub fan-out tree: the aggregation layer publishes one message per poll update to a topic; a layer of broadcast/relay nodes subscribed to that topic each push to their own local shard of connected clients, turning an O(N) fan-out from a single node into a distributed, parallel fan-out across many gateway nodes, each handling only its own slice of connections.
Hot Partition / Hot Poll Problem
If a single, wildly popular poll dominates traffic (which is the common case — one poll open at a time to the whole audience), naive partitioning by poll_id alone concentrates all traffic on one partition. The fix is composite partitioning, e.g. hash(poll_id, attendee_id) % N, spreading a single poll’s vote stream across many partitions and many aggregation workers, then merging (which is safe and cheap thanks to the CRDT/shard-counter design above).
50,000 attendees, poll open for 10 seconds, 80% participation: 40,000 votes / 10s ≈ 4,000 votes/s average, but realistic bursts run 3–5× higher near the start of the window — provision (and load-test) for the order of 15,000–20,000 votes/sec sustained for a few seconds, not just the average.
Thundering Herd on Poll Open
The single sharpest spike in the entire system is the moment a presenter clicks “launch poll”: tens of thousands of clients receive a near-simultaneous WebSocket push telling them a new poll is available, and a large fraction submit a vote within the same one-to-two second window as their UI renders and they tap an answer. Two mitigations are standard: (1) the “poll opened” notification itself is fanned out through the same pub/sub tree used for results, so it scales the same way results do, and (2) client-side jitter — deliberately adding a few hundred milliseconds of random delay before the very first UI interaction becomes possible, or simply relying on natural human reaction-time variance — smooths what would otherwise be a single-instant spike into a slightly wider window, reducing peak instantaneous load on ingest.
Connection Scaling Math
If a single gateway node comfortably sustains 20,000 persistent connections with periodic (sub-second) broadcast traffic, 50,000 attendees requires at least 3 gateway nodes at capacity — but production sizing adds headroom for (a) uneven load balancing across nodes, (b) at least N+1 (ideally N+2) redundancy so a single node failure doesn’t cause a connection drop cascade onto already-full neighbors, and (c) reconnect storms, where a large fraction of clients reconnect within a short window after any gateway restart or network event — meaning real-world capacity planning for 50,000 attendees often provisions for 5–6 nodes’ worth of headroom, not the bare-minimum 3.
Shed early, queue durably
Reject at the edge (rate limiter, idempotency check) before touching the broker; once accepted, the write is durable and can be replayed.
Parallelize by composite key
hash(poll_id, attendee_id) partitioning turns a hot poll into many cool partitions that CRDT-merge safely at the end.
Broadcast, don’t unicast
Publish one snapshot per interval to a topic; let each gateway push it to its own connected slice — O(N) fan-out becomes O(N/k) per node.
Jitter and reconnect politely
Exponential backoff with jitter on reconnect prevents a naive stampede after any gateway blip.
Keep the hot set small
Only currently-open polls live in Redis at full fidelity; closed polls demote to the durable log immediately, keeping memory bounded.
Provision for the reconnect wave
Bare-minimum node count survives steady state; N+2 survives a real incident, including the reconnect storm that follows one.
- “Back-of-envelope: how many aggregation workers do you need?” — A good answer walks through peak votes/sec ÷ sustainable throughput per worker (accounting for CRDT merge cost and Redis round-trip time), then adds headroom for worker failure and rebalancing.
- “What’s your bottleneck as attendee count grows from 50,000 to 500,000?” — Connection count on the gateway tier and fan-out bandwidth typically become the limiting factor before the aggregation/ingest path does, since votes/sec grows roughly linearly with attendees but so does the number of sockets needing pushed updates.
High Availability & Reliability
Events are scheduled and non-negotiable — going dark mid-keynote is a public incident. Reliability engineering here is not paranoia; it is table stakes.
- Stateless gateways and ingest services: Any node can be killed and replaced without losing in-flight business state, since durable state lives in the broker, the vote log, and Redis — enabling rolling deploys and fast auto-healing.
- Multi-AZ (and ideally multi-region) broker and cache clusters: The message broker and Redis cluster are deployed across availability zones with replication, so a single zone outage doesn’t halt an in-progress event.
- Graceful degradation ladder: If the aggregation layer falls critically behind or Redis is unhealthy, the system degrades to showing “last known good” results with a subtle staleness indicator, rather than showing an error or blank chart — availability of a slightly-stale answer beats unavailability.
- Client-side reconnect with exponential backoff and jitter: WebSocket clients automatically reconnect and resubscribe on drop, with backoff to avoid a reconnect storm overwhelming gateways right after a network blip or gateway restart.
- Circuit breakers between tiers: The ingest service trips a circuit breaker to shed load (returning a fast, honest “try again” instead of votes queueing indefinitely) if the broker is unreachable, protecting upstream clients from cascading timeouts.
- Idempotent replay for disaster recovery: Because the vote log is the source of truth and aggregation is a pure, deterministic function over it, the entire aggregate state for any poll can be rebuilt by replaying the log — a powerful recovery mechanism if the cache layer is lost entirely.
- “The event is live, on stage, in front of 50,000 people, and Redis just fell over. What happens?” — Walk through the degradation ladder: gateways keep serving the last cached snapshot, ingest keeps durably queuing votes to the broker (nothing is lost), and once Redis (or its replacement) is back, aggregation workers replay/catch up from the broker offset and resume publishing — attendees see a brief freeze, not an error, and no votes are lost.
Disaster Recovery Runbook, Conceptually
A well-designed system in this space treats “the aggregation cache is gone” as a routine, rehearsed scenario rather than a five-alarm emergency, precisely because the architecture was built with that recovery path in mind from day one:
- Detect via consumer lag and cache health alerts crossing threshold, or an outright node/cluster failure signal.
- Ingest continues accepting and durably queuing votes to the broker without interruption — this tier has no dependency on the cache being healthy.
- Gateways serve the last known-good snapshot to connected clients with a subtle “updating” indicator rather than an error state, preserving perceived availability.
- Once the cache layer is restored (new nodes, failover to a replica, or a fresh cluster), aggregation workers resume consuming from their last committed broker offset and rebuild the in-memory aggregate by replaying events forward — an operation bounded by however much backlog accumulated during the outage.
- Once aggregation workers report caught up (lag back near zero), the fan-out layer resumes normal-cadence broadcasting and the “updating” indicator clears.
The critical property enabling this whole runbook is that the aggregate is always a deterministic, replayable function of the durable log — nothing about recovery requires guessing or manual data reconciliation.
Durable vote log is the single source of truth; in-memory aggregate is a derived cache
Context: Live aggregates in Redis are fast but volatile; a cache-only design has no principled recovery story after a node loss.
Decision: The append-only vote log (partitioned message broker + wide-column durable store) is authoritative. Redis holds only a derived projection that can be rebuilt at any time by replaying the log.
Consequence: Recovery from cache loss is a rehearsed, bounded operation rather than data loss. Poll-close final tallies always compute from the log, giving an exact answer even if the live aggregate drifted.
Security
Public, time-boxed, high-visibility events are an attractive target. Security here is layered defense across identity, ingest, transport, moderation, and the edge.
Authentication & session binding
Every vote/question is tied to an authenticated attendee session (event registration token or short-lived JWT), preventing anonymous ballot stuffing.
One-vote-per-attendee enforcement
Enforced server-side via the idempotency mechanism described earlier — never trust a client-side “already voted” flag alone.
Rate limiting & bot detection
Per-connection and per-IP rate limits, combined with behavioral heuristics (impossible vote velocity, identical timing patterns) to catch scripted vote manipulation.
Content moderation for Q&A
Automated profanity/toxicity filtering plus a human moderator queue before questions surface to the whole audience, since open text input at this scale is a predictable abuse vector.
TLS everywhere
TLS everywhere, including on the WebSocket upgrade; short-lived tokens rather than long-lived credentials on the socket.
Tenant/event isolation
Strict authorization checks that an attendee’s vote can only affect the poll belonging to the event they’re actually registered for, preventing cross-event data leakage or manipulation in a multi-tenant SaaS deployment.
DDoS protection at the edge
Since these events are public, high-visibility, and time-boxed, they’re an attractive DDoS target; edge scrubbing and aggressive anomaly-based throttling protect the origin before traffic even reaches application services.
Threat Model Summary
| Threat | Mitigation |
|---|---|
| Scripted/bot ballot stuffing (one identity voting repeatedly via automation) | Server-side idempotency keyed on authenticated identity; velocity-based anomaly detection |
| Sybil attack (many fake registrations to gain many “legitimate” votes) | Registration-level fraud checks tied into the event registration system, outside this system’s direct scope but a required upstream dependency |
| Offensive or harassing content submitted via open-text Q&A or word-cloud polls | Automated toxicity/profanity filtering at ingest, plus a human moderation queue before wide visibility |
| Cross-event data leakage in a multi-tenant deployment | Strict per-event authorization checks on every read and write, enforced at the ingest and query layers, not just the UI |
| Connection-flood / DDoS against the WebSocket gateway during a high-visibility event | Edge-layer scrubbing, per-IP connection rate limits, autoscaled gateway capacity with pre-warming ahead of scheduled events |
| Replay of a captured vote request to inflate a count | Idempotency key plus short-lived, single-use-adjacent tokens tied to the session |
Q&A Moderation Pipeline
Open-text Q&A deserves its own mini-pipeline distinct from vote ingest, because it carries reputational risk that a wrong multiple-choice vote count simply doesn’t: a single offensive question broadcast to 50,000 attendees on a company’s public town hall is a real incident. A layered approach is standard: (1) a fast, automated classifier screens submissions synchronously and blocks the most clearly disallowed content before it’s even stored; (2) borderline content is queued for a human moderator, visible only to the moderator until approved; (3) approved content becomes visible to the full audience and enters the same upvote-ranking aggregation pipeline as any other question; (4) a “report” action lets attendees flag already-visible content for expedited moderator review, since no automated filter catches everything.
- “How do you stop someone from writing a script to vote 10,000 times?” — Layered defenses: authenticated, per-registered-attendee identity plus server-side idempotency (identity, not client state, defines “already voted”), rate limiting, and anomaly detection on vote velocity/timing — no single layer is sufficient alone.
- “Why not just let all Q&A questions appear instantly, the way votes are counted instantly?” — Because open text carries qualitatively different risk than a bounded multiple-choice selection; the trade-off between “live and unmoderated” and “delayed but safe” is a product decision as much as a technical one, and most production systems land on a lightweight pre-screen plus post-hoc reporting rather than either extreme.
Monitoring, Logging & Metrics
You can’t operate a system like this from an intuition; you operate it from dashboards, traces, and calendar-aware alerts that know when an event is live.
- Golden signals per tier: latency, traffic (votes/sec, active connections), error rate, and saturation (broker consumer lag, Redis memory/CPU, connection count vs. capacity) tracked separately for ingest, aggregation, and fan-out.
- Consumer lag as the critical leading indicator: Message broker consumer lag for the aggregation workers is the single most important metric — rising lag is the earliest, clearest signal that live results are about to fall visibly behind reality.
- End-to-end latency tracing: Distributed tracing (with a correlation ID attached at vote submission) to measure the full path from “vote accepted” to “reflected in a broadcast the client received,” not just the isolated latency of a single hop.
- Real-user monitoring (RUM): Client-side timing of “time from tap to seeing the chart move” captured from actual attendee devices, since server-side metrics alone can miss client network/rendering delays.
- Structured, sampled event logging: High-cardinality vote/question events are logged in a structured, sampled way to the analytics store, avoiding the cost of logging every single event at full fidelity to a system meant for debugging.
- Alerting thresholds tied to event calendars: Since load is extremely spiky and tied to scheduled events, static always-on alert thresholds create noise; alerting is often tied to “is an event currently live” context to distinguish real incidents from expected idle-period quiet.
Example SLOs
| Metric | Target |
|---|---|
| Vote submission acknowledgement latency (p99) | < 200ms |
| Vote-to-broadcast end-to-end latency (p95) | < 1.5s |
| Vote durability (accepted votes never lost) | 100% |
| WebSocket connection success rate | > 99.9% |
| Aggregation consumer lag during peak | < 3s, self-healing within 30s of a spike subsiding |
Deployment & Cloud
Because traffic is spiky and event-scheduled, deployment strategy is inseparable from the event calendar.
- Container orchestration (Kubernetes): Stateless services (ingest, gateway, aggregation workers) run as horizontally scalable Deployments with Horizontal Pod Autoscaling driven by custom metrics like votes/sec and connection count, not just CPU.
- Pre-scaling / scheduled scale-up: Because major events are scheduled in advance, capacity is often proactively scaled up ahead of a known event start time rather than relying purely on reactive autoscaling, which can lag behind an instantaneous traffic cliff-edge at “poll opens.”
- Managed streaming and cache services: Using a managed Kafka/Kinesis-equivalent and managed Redis/ElastiCache-equivalent reduces operational burden for the stateful backbone components that are hardest to run reliably in-house.
- Multi-region deployment for global audiences: Regional gateway clusters close to attendee geography reduce WebSocket latency; the aggregation/streaming backbone can be regional-per-event (simplest) or globally replicated for events with a truly global, latency-sensitive audience.
- Blue-green or canary deploys, never mid-event: Deployments of the hot-path services are scheduled around the event calendar — rolling out a change to the aggregation service five minutes before a 50,000-person keynote poll is a well-known way to cause an incident.
Cost Optimization
Traffic for this kind of system is extremely spiky and largely idle between scheduled events, which makes naive “always provisioned for peak” capacity wasteful. Common cost levers include: scaling stateless tiers (gateway, ingest, aggregation workers) down to a small baseline outside event hours and scaling up ahead of scheduled start times; using spot/preemptible capacity for aggregation workers where possible, since they’re stateless and can be replaced without data loss thanks to the durable log; and tiering storage so that hot, actively-queried data (the currently open poll) sits in memory while closed-event historical data moves to cheaper, colder storage after some retention window, keeping the expensive in-memory cache small and fast rather than growing unbounded across every event the platform has ever hosted.
Multi-Tenancy Considerations
A platform serving many customers’ events simultaneously (as opposed to a single internal deployment) needs the architecture above applied per-tenant with careful isolation: partition keys and cache keys are always namespaced by event_id (and often by customer/org ID above that), noisy-neighbor protection ensures one customer’s viral 100,000-attendee event doesn’t starve resource quota from a dozen smaller concurrent events on shared infrastructure, and rate limits and quotas are enforced per tenant rather than globally so abuse or a runaway bug in one customer’s integration can’t degrade the platform for everyone else.
Never deploy hot-path services within the pre-event freeze window (typically the 30–60 minutes before a major event starts). The blast radius of a bad rollout at exactly the wrong moment is orders of magnitude worse than the delay of shipping a change an hour later.
Databases, Caching & Load Balancing
Different data has different access patterns; using one store for everything is exactly the trap this design avoids.
| Data | Store | Why |
|---|---|---|
| Live/hot aggregate per active poll | Redis Cluster (sharded counters, sorted sets for Q&A ranking) | Sub-millisecond atomic operations, native data structures fit the access pattern |
| Immutable, append-only vote/question events | Wide-column store (Cassandra/DynamoDB-style) or the broker’s own durable log | Write-optimized, horizontally scalable, natural fit for high-volume append-only data |
| Event/session/poll/user metadata | Relational DB (PostgreSQL) | Strong consistency and relational integrity for comparatively low-volume, structured data |
| Post-event analytics/reporting | Columnar analytics store (ClickHouse/BigQuery-style) | Optimized for large aggregate scans and reporting queries, isolated from the live path |
Load balancing operates at two distinct layers: a standard L4/L7 load balancer distributes initial HTTP and WebSocket-upgrade requests across gateway nodes (often with consistent hashing so a reconnecting client tends to land near the same broadcast group, improving cache locality); and a separate, logical “topic-to-node” mapping in the pub/sub layer ensures broadcast fan-out work is spread evenly across gateway nodes rather than concentrated on whichever node happened to receive the aggregation service’s publish call.
APIs & Microservices
Two protocols, two very different jobs — and internal services drawn along the same fault lines they’ll fail along.
REST/HTTP API
Poll/event CRUD, authentication, historical results export — standard resource-oriented endpoints suited to request/response semantics.
WebSocket API
A thin subscribe/publish protocol (subscribe to a poll’s topic, receive snapshot/delta messages) deliberately kept minimal so the gateway tier stays simple, stateless, and easy to scale.
Internal service boundaries
Ingest, aggregation, and fan-out are separate microservices specifically because they scale differently and fail independently — an aggregation-layer bug or slowdown should not be able to take down the ability to accept and durably queue new votes.
Backpressure-aware contracts
Internal service calls between ingest and the broker are fire-and-forget publishes (not synchronous RPC waiting on aggregation), which is what allows the ingest tier’s latency to stay flat even when downstream aggregation is temporarily behind.
Design Patterns & Anti-patterns
Every real production system is a chosen set of patterns and an avoided set of anti-patterns. Name them explicitly.
Patterns Used
Command Query Responsibility Segregation
The “write a vote” path and the “read the aggregate” path are entirely separate services with separate data models — a foundational pattern for this whole design.
Append-only log as truth
The durable vote log is the append-only source of truth; the aggregate is a derived, rebuildable projection.
Publish / Subscribe
Decouples the aggregation layer (publishers) from potentially thousands of gateway nodes (subscribers) delivering to clients.
Sharding / Partitioning
Applied at the counter level (shard counters) and the stream level (partitioned broker topics) to remove single-point contention.
Circuit breaker & bulkhead
Isolate failures in one tier (e.g., aggregation) from cascading into another (e.g., ingest).
Conflict-free replicated types
G-Counter / PN-Counter for vote tallies and top-K structures for Q&A ranking give safe, order-independent merging.
Anti-patterns to Avoid
Making the client wait for the vote to be fully aggregated and reflected before acknowledging couples ingest latency to aggregation latency and destroys throughput.
Having 50,000 clients GET /results every second is functionally a self-inflicted DDoS.
Works great in a demo with 10 users, collapses immediately at real event scale — a classic case of premature optimization’s evil twin, premature under-engineering.
Purely client-side vote-guarding is trivially bypassed and must always be backed by server-side enforcement.
Applying strict, synchronous consistency everywhere (including the live/open-poll display) sacrifices the performance headroom needed at scale, when only the poll-close moment truly needs it.
Best Practices & Common Mistakes
The design above is only as good as its operational habits. These are the disciplines that separate an event that runs boringly from one that trends on social media for the wrong reasons.
Do
- Load test with realistic burst shape, not average load — steady-state average votes/sec dramatically understates the 5–10 second spike right after a poll opens; test the spike, not just the mean.
- Pre-warm capacity ahead of known event start times rather than relying solely on reactive autoscaling that may lag the traffic cliff-edge.
- Make the broadcast cadence configurable per event size — a 200-person webinar can broadcast every vote; a 50,000-person keynote should batch on a fixed interval.
- Always compute the final, closing tally from the durable log, never from the approximate streaming aggregate, however close they usually are.
- Design the client for graceful reconnection with backoff/jitter; a naive immediate-retry-on-disconnect client is a common cause of “thundering herd” reconnect storms after any gateway blip.
Common mistakes
- Ignoring Q&A as “just a simpler poll.” Open-text Q&A introduces moderation, ranking, and abuse-resistance requirements that a multiple-choice poll doesn’t have; treating them identically under-serves both.
- Forgetting per-tenant/per-event isolation in a multi-event SaaS platform, which can leak or cross-contaminate data between simultaneously running events sharing the same infrastructure.
- Load-testing only the aggregation service in isolation instead of the full ingest → broker → aggregate → fan-out path with realistic connection counts.
- Optimizing purely for the “happy path” and skipping the reconnect / cache-loss / broker-lag scenarios — those are exactly when the system will be judged.
Real-World Industry Examples
No two production systems in this space are built identically, but the recurring architectural DNA — decoupled ingest, sharded/mergeable aggregation, and push-based fan-out over persistent connections — shows up consistently across the industry, because it’s a direct consequence of the same underlying constraints (high fan-in, high fan-out, tight latency budget) rather than any single company’s specific implementation choice.
Slido / Mentimeter
Purpose-built live polling and Q&A platforms embedded into webinar tools and conferencing software; their core engineering challenge is exactly this design — fast fan-in of votes, real-time aggregate fan-out to a shared screen and thousands of individual devices, and Q&A moderation at conference scale.
Zoom / Microsoft Teams
Large video conferencing platforms bolted real-time polling onto existing massive-scale, low-latency infrastructure originally built for audio/video, reusing their existing signaling and pub/sub backbones for poll result delivery to avoid building a second parallel real-time system from scratch.
Twitch / YouTube Live
Live streaming platforms handle a structurally similar problem (extremely high fan-in chat messages, extremely high fan-out to millions of viewers) and popularized many of the sharded pub/sub and CRDT-style counter techniques described here, at even larger scale.
Kahoot!
A live quiz platform whose core technical challenge — collecting simultaneous answers from tens of thousands of participants (often students) within a tight countdown window and instantly showing a leaderboard — is essentially the same ingest/aggregate/fan-out pipeline, with the added wrinkle of scoring based on answer speed.
Reddit-style vote systems
The sharded-counter and eventually-consistent-then-reconciled approach to vote tallies at massive scale (popularized publicly in various engineering blog posts across the industry) is the same underlying pattern applied here to live event polling.
None of these companies invented these patterns in isolation. What they did do is invest heavily in load testing, calendar-aware operations, and honest post-incident writeups — which is how the same handful of patterns became the industry default.
FAQ
The questions candidates and product managers actually ask, answered honestly.
Why not just use a simpler request/response model where the client fetches the latest results every second?
At tens of thousands of concurrent attendees, that turns into tens of thousands of requests per second just for polling, most of which return “nothing changed,” wasting enormous server and network capacity compared to a push-based model where updates are sent only when there’s something new to send.
Is eventual consistency really acceptable for something as visible as a live vote count?
Yes, during the “live” phase — humans perceive a chart updating within a second as “real time” even if it’s technically a fraction of a second behind reality, and the system switches to a strongly consistent, exact computation at the moment the poll closes and the final number actually matters.
How do you handle an attendee who joins the event late, after a poll has already been open for a minute?
On WebSocket connect/subscribe, the gateway (or aggregation service) sends a full current snapshot of the poll’s aggregate state before switching the client to incremental delta updates, so late joiners see accurate current results immediately rather than starting from zero.
What happens to votes cast in the last second before a poll is force-closed by the presenter?
The close operation is applied at a specific logical timestamp/offset in the durable vote log; votes with a server-received timestamp after that cutoff are excluded from the final tally, and the ingest service can immediately start rejecting further votes for that poll with a clear “poll closed” response.
Could this be built entirely serverless (e.g., managed functions plus managed pub/sub)?
Yes for many of the stateless tiers (ingest validation, snapshot publishing), which is a common and cost-effective choice for the highly bursty, idle-most-of-the-time traffic pattern of scheduled events; the stateful backbone (broker, cache cluster) typically still runs as a managed clustered service rather than as functions, since it needs persistent, low-latency state.
How is Q&A upvote ranking different from vote counting, from a systems perspective?
Vote counting only needs a small, fixed set of totals (one per poll option). Q&A ranking needs an ordered top-K over a potentially large and growing set of distinct questions, which is a heavier data structure (sorted set / priority structure) than a flat counter, and typically only needs to be exactly correct for the handful of items actually visible on screen rather than the entire long tail.
What if the presenter’s own connection drops during a live poll?
The presenter console is just another WebSocket subscriber to the same broadcast topic as attendees, so on reconnect it receives a fresh snapshot exactly like any other client — the poll itself keeps running and collecting votes on the server side regardless of whether any particular viewer, including the presenter, is currently connected.
How would you extend this design to support breakout-room-level or region-level segmented results, not just one global tally?
Extend the partition/shard key to include the segment (e.g., hash(poll_id, segment_id, attendee_id)) and maintain a separate CRDT aggregate per segment in addition to the global one; the pub/sub topic model extends naturally by adding a per-segment topic alongside the event-wide topic, so clients subscribe to whichever slice is relevant to them.
Is a message broker like Kafka strictly necessary, or could a simpler in-memory queue work?
For a small internal tool with modest scale and no durability requirement, an in-memory queue is a reasonable simplification. At the scale this design targets, the durable, replicated, partitioned log is what provides both shock-absorbing capacity for traffic bursts and a replayable source of truth for recovery and exact final tallies — properties a plain in-memory queue doesn’t give you, since a crashed process would simply lose whatever was queued.
Summary & Key Takeaways
The whole design is a composition of well-known patterns matched carefully to the specific shape of the workload — a bursty, high-fan-in write spike followed immediately by a sustained, high-fan-out read pattern to the same population.
Ingest / Aggregate / Fan-out
Treat the problem as three distinct sub-problems with different scaling shapes and design each independently.
Broker as shock absorber
Decouple vote acceptance from result computation using a partitioned, durable message broker; this is what lets the system absorb massive, bursty write spikes without data loss.
Sharded, mergeable counters
Use sharded, mergeable counters (CRDTs) to make aggregation embarrassingly parallel and safe under at-least-once delivery.
Push, don’t pull
Persistent connections and periodic broadcast snapshots keep fan-out cost bounded regardless of vote volume.
Key takeaways
- Three sub-problems, three scaling shapes. Ingest (fan-in), aggregate (stream processing), and fan-out (broadcast) each deserve their own design and their own load test.
- The broker is the shock absorber. A durable, partitioned log lets ingest stay flat while aggregation catches up, and lets any downstream tier be rebuilt from replay.
- Sharded / CRDT counters, not a single global lock. Contention-free writes, cheap bounded reads, safe merging under any delivery order.
- Push over persistent connections. Polling by 50,000 clients is a self-inflicted DDoS; a broadcast topic push per interval is cheap and predictable.
- Consistency is tiered on purpose. Eventually consistent while the poll is live; strongly consistent (recomputed from the log) the instant it closes.
- Reliability comes from statelessness at edges, durability at the core. Every tier degrades gracefully; the durable vote log guarantees nothing is ever silently lost.
- Security is first-class. One-vote-per-identity, rate limiting, and moderation are architectural concerns, not bolt-ons.
- Operate around the event calendar. Pre-warm capacity, freeze deploys, alert only when an event is actually live — treat time itself as an input to the system.
Pulled together, none of these individual pieces — a message broker, a sharded counter, a pub/sub fan-out tree — is exotic on its own. What makes this a genuinely interesting system design problem is composing them correctly around the specific shape of the workload: a short, extremely bursty write spike from tens of thousands of independent sources, immediately followed by a sustained, high-fan-out read pattern to the same population, all under a latency budget tight enough that the result still feels “live” to a room full of people watching a shared screen.
Strong candidates don’t reach for “just make everything strongly consistent” and don’t reach for “just cache it in Redis” either. They match the tool to the sub-problem — broker for ingest, sharded counter for aggregation, pub/sub for fan-out — and are honest about which stage is eventually consistent and which stage is exact.