MKSAP Live Polling
Real-time audience polling for medical board review sessions. Built with React, Cloudflare Workers, Durable Objects, and WebSockets.
TL;DR: TL;DR: I built a real-time polling system for MKSAP (medical board exam) style questions. Presenters show questions, audiences vote A/B/C/D, and results update live via WebSockets. Built on Cloudflare Workers with Durable Objects for persistence.
The Problem
Medical education sessions often use audience response systems, but existing solutions are:
- Expensive: $200+/month for enterprise polling tools
- Clunky: Require app downloads or complex login flows
- Slow: Results take seconds to update
- Overkill: We just need A/B/C/D voting, not surveys
I needed a simple, fast, real-time polling tool for board review sessions.
My Approach
I built a minimal system with two interfaces:
- User page (
/user): Simple A/B/C/D buttons, one vote per session - Presenter page (
/presenter): Real-time bar chart, reset button
WebSockets handle instant updates—votes appear on the presenter screen as they're cast.
Cloudflare Durable Objects provide the perfect persistence model: state lives in a single location, survives restarts, and scales automatically.
Architecture
MKSAP Live Polling - Architecture Diagram
Key Features
- One Vote per Session: localStorage tracks if user already voted
- Real-time Updates: Votes appear instantly on presenter screen
- Auto-reconnect: Exponential backoff (up to 30s) on disconnect
- Vote Reset: Presenter can clear votes for next question
- Bar Chart Visualization: Live-updating vote counts
- No Login Required: Just open the URL and vote
- Edge Deployment: <50ms latency globally
Results & Metrics
| Metric | Value |
|---|---|
| Vote Types | A, B, C, D |
| Update Latency | <100ms |
| Max Reconnect Delay | 30 seconds |
| Max Reconnect Attempts | 10 |
| Persistence | SQLite in Durable Object |
| Deployment | Cloudflare Edge (global) |
What I Learned
The trickiest part was WebSocket lifecycle management. Connections drop unexpectedly, and users navigate away mid-session. I implemented robust reconnection:
// Exponential backoff with jitter
const reconnect = () => {
const delay = Math.min(30000, 1000 * Math.pow(2, attempts));
const jitter = delay * 0.1 * Math.random();
setTimeout(connect, delay + jitter);
attempts++;
};
Durable Objects were perfect for this use case. Unlike traditional databases:
- State is co-located with the WebSocket handler
- No cold start for connected clients
- SQLite provides ACID guarantees
- Automatic scaling per poll room
The single-vote enforcement using localStorage is simple but effective for casual use. For high-stakes voting, you'd want authenticated sessions.
Frequently Asked Questions
What problem does MKSAP Live Polling solve?
It provides a simple, free, real-time audience response system for medical education sessions. Presenters show questions, audiences vote A/B/C/D, and results update instantly.
What technologies power this project?
React with Vite for the frontend, Cloudflare Workers for serverless backend, Durable Objects with SQLite for stateful persistence, and native WebSockets for real-time communication.
How does it handle disconnections?
The WebSocket client implements exponential backoff reconnection with jitter. It attempts up to 10 reconnects with increasing delays (1s, 2s, 4s... up to 30s max). On reconnect, the client receives the current vote state.
Frequently Asked Questions
More Projects
View allBuilt by Abhinav Sinha
AI-First Product Manager who builds production-grade tools. Passionate about turning complex problems into elegant solutions using AI, automation, and modern web technologies.