Project
Movie Poll
Cast a vote and watch it move — this poll runs over a persistent WebSocket connection through API Gateway, Lambda, and DynamoDB. No polling, no refreshing: when a vote lands anywhere, every open tab watching this page updates on its own.
Demo
If you were a movie, what movie would you be?
Pick the one that fits. Your vote goes out over an open WebSocket connection to a Lambda function, updates the tallies in DynamoDB, and gets broadcast back to every connected browser — including this one — in real time.
Each option pairs a movie with a one-line description of the mood or energy behind it — not a plot summary, more of a vibe check. Pick whichever one matches how you're feeling right now.
Privacy notice: your browser stores a random,
anonymous ID in localStorage — not a cookie — so the
poll remembers which option is yours. Nothing personally
identifiable is collected, and it's never sent anywhere beyond
this one poll.
Changed your mind? Tap a different option any time — your vote moves with you.
Try this: open this page in a second tab and vote there. Watch this tab's chart update on its own — that's the WebSocket push, not a page refresh.
Loading live results…
Live activity
Architecture
How it works
This isn't a series of one-off requests — it's a single WebSocket connection that stays open the whole time you're on this page. Step 1 happens once, when you arrive. Steps 2 through 5 repeat every single time anyone votes, without ever reconnecting. Click any numbered step to jump to it, or let it play through on its own.
-
1
Connect
Your browser opens a WebSocket connection to API Gateway when this page loads — this happens once. It stays open, ready for anything that happens next, until you leave.
-
2
Vote sent
Tapping an option sends a small message over that already-open connection to a Lambda function — no new request, no reconnecting, no page reload.
-
3
Tally updates
Lambda records your choice and updates the shared vote counts in DynamoDB — changing your vote moves the count instead of adding a duplicate.
-
4
Tallies read back
That same Lambda function reads the fresh totals for every option back out of DynamoDB, ready to send.
-
5
Broadcast
Lambda pushes the updated results back out through API Gateway to every open connection at once — including yours, without you doing anything.
Steps 2 through 5 can repeat any number of times — that's what happens every time anyone votes. The connection itself only closes when you actually leave the page, at which point it disconnects cleanly rather than staying open in the background.
Use cases
Where this pattern fits
This real-time push pattern isn't specific to a movie poll — the same WebSocket API + Lambda + DynamoDB combination covers anything that needs to update instantly across multiple viewers at once.
Live audience Q&A and polling
Conference talks, webinars, or classrooms where the audience votes or asks questions and everyone sees results shift live, without refreshing anything.
Real-time dashboards
Order status boards, live metrics, or ticket queues that need to reflect changes the instant they happen instead of on a fixed refresh interval.
Collaborative & multiplayer features
Shared cursors, "who's online" presence indicators, or lightweight multiplayer state — anywhere multiple people need to see the same live picture at once.
Pricing
What this actually costs
Every piece of this architecture is pay-per-use, but WebSocket connections add a cost dimension a plain HTTP API doesn't have: you pay for connection time, not just requests. These are estimates based on public AWS list pricing, not a guarantee.
Low volume
~$0/mo
Casual portfolio traffic — a handful of visitors keeping a tab open here and there. Comfortably inside free-tier-level cost for Lambda and DynamoDB, with negligible WebSocket charges.
Moderate volume
~$1–3/mo
A steady trickle of visitors, or a short burst — a few hundred connection-minutes and a few thousand messages a month.
High volume
~$10–25/mo
A viral moment with thousands of concurrent connections — connection-minutes and broadcast costs both start to add up.
What drives the cost
- API Gateway (WebSocket) — billed per million messages sent, plus per million connection-minutes for every second a socket stays open, even idle.
- Lambda — three functions (connect, disconnect, vote), each billed per request and per millisecond of compute time.
- DynamoDB — on-demand pricing per read/write; the vote broadcast currently scans the full connections table on every vote, which is fine at demo scale but is the first thing that'd need to change at real scale.
- S3 + CloudFront — a few cents a month at most, same as the rest of this static site.
Design decisions
Why I built it this way
Every architecture involves tradeoffs. Here's the reasoning behind some of the choices in this project — including a couple I only landed on after real deployment testing proved the original plan wrong.
A WebSocket API instead of polling
Having the browser poll for updates every few seconds would have been simpler to build. I chose the WebSocket API anyway, because the payoff is worth the extra complexity: two tabs on this page update each other instantly, with no visible lag — a much stronger demonstration of real-time push architecture than a polling loop would be.
Three DynamoDB tables instead of one
Connections, votes, and tallies each answer a different question, so each gets its own table with its own natural key. DynamoDB supports "single-table design" for cases where minimizing table count matters at scale, but for a project this size, three small, clearly-named tables are easier to read and reason about than one table doing three jobs at once.
Separate Lambda functions per event
$connect, $disconnect, and
vote/sync each get their own
function instead of one large Lambda branching on event
type. Smaller functions are easier to read in isolation,
and CloudWatch logs stay naturally separated by concern
instead of interleaving unrelated events.
Sync-after-connect, not push-from-connect
The first version tried to push initial results to a
client from inside its own $connect
invocation — and it turned out that's an unreliable
pattern in API Gateway's WebSocket API; a connection isn't
always immediately postable during its own connect event.
Real deployment testing caught this, not local reasoning
alone. The fix: the client asks for results in a separate
message right after opening, which sidesteps the race
entirely.
Sequential writes instead of a transaction
Recording a vote and updating both the new and old option's tallies happens as two or three separate DynamoDB calls, not one atomic transaction. A production vote system handling real stakes should use a transaction so a mid-request failure can't leave the tally out of sync. For a demo poll, the added complexity wasn't worth it — and that tradeoff is documented directly in the code, not left as a silent shortcut.
Connect always emails; disconnect carries the stats
Early on, every single vote sent me an email — useful while debugging, but far too noisy for something people would actually vote on. Connect events stay lightweight (just confirming someone showed up), and the full results snapshot moved to disconnect, so I get one meaningful summary per visit instead of one email per click.
A movie poll instead of a dry sample dataset
The architecture didn't need a fun topic to work, but a portfolio piece does more work when people actually want to interact with it. A quick, low-stakes, slightly funny question gets more real votes than a generic "Option A vs Option B" would — and that matters when the whole point is demonstrating the system live.
A config.js scoped to this project alone
Rather than one shared config file for the whole site, this project keeps its own, holding only what it needs. As more projects get added here, a single growing config file would start implying a coupling between unrelated backends that doesn't actually exist.