Skip to main content
MMujtaba
Back to Projects

Poldit — Real-Time Polling & Community Discussion

Web AppSaaS
ReactNestJSPostgreSQLWebSocketsRedisAWS

1M+

Total Votes

50K+

Real-time Users

<20ms

Vote Latency

Overview

Poldit is a real-time polling and community discussion platform where users can create polls, vote, and debate results — all live. Popular polls attract tens of thousands of simultaneous voters, making live result visualization the core product experience. I led the backend engineering on a three-person team.

The Challenge

The naive implementation — write every vote to PostgreSQL, query the count, push to connected clients — falls apart quickly at scale. A viral poll with 50K concurrent voters means 50K simultaneous write transactions and 50K SELECT COUNT queries per result update cycle. PostgreSQL would be on its knees within minutes.

We also needed to prevent vote manipulation: users gaming polls with multiple accounts or automated bots was a real concern that could undermine the platform's credibility.

Architecture & Technical Decisions

Redis-First Vote Counting

I moved the hot vote counting path entirely into Redis. When a user votes, the backend increments a Redis counter (INCR) and pushes the user's vote to a Redis Set (for deduplication). A background job flushes accumulated votes to PostgreSQL every 5 seconds in a single batch write. This reduced PostgreSQL write load by 97% during peak traffic.

  • INCR on Redis counters for O(1) vote tallying
  • SADD to user-vote Sets for server-side deduplication per poll
  • Lua script for atomic check-and-increment (prevents race conditions)
  • 5-second flush job writes batched votes to PostgreSQL for persistence
  • Redis persistence (AOF) as safety net between flush cycles

Live Result Broadcasting

Rather than pushing on every vote, the backend broadcasts updated result percentages every 500ms using a scheduled ticker per active poll. This smoothed the update rate and prevented a thundering herd of WebSocket pushes. Clients received a diff payload (only changed option percentages) rather than the full results object, minimizing payload size.

  • 500ms broadcast cadence per active poll room
  • Diff-based payloads: only changed percentages sent over the wire
  • Room pruning: inactive poll rooms (no votes in 30s) stop broadcasting
  • Socket.IO with Redis adapter for multi-instance WebSocket scaling

Anti-Fraud Layer

Vote integrity required a layered approach. At the network level, we fingerprinted connections using IP + User-Agent + timing heuristics and rate-limited aggressively. At the application level, authenticated users could only vote once per poll (enforced by Redis Set + PostgreSQL constraint). Suspicious vote velocity triggered a CAPTCHA challenge before the vote was accepted.

Results

  • 1M+ total votes processed across the platform at <20ms p99 vote acknowledgement
  • Handled 50K concurrent voters on a single viral poll without degradation
  • PostgreSQL write load reduced 97% vs. naive implementation
  • Zero vote corruption incidents — Redis Lua atomicity held in all tested edge cases
  • Bot/fraud vote rate: <0.3% after anti-fraud layer deployed

What I Learned

Counter-intuitively, making a voting system fast and accurate requires accepting eventual consistency in the right places. The 5-second flush delay means vote counts in PostgreSQL are slightly stale — but users watching the live chart don't care about PostgreSQL; they care about the Redis-sourced number on screen. Separating "what users see" from "what the database holds" and designing each independently is a powerful mental model.

Related Projects