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.

Connecting…
0 votes

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.

    Browser
    API Gateway
    Lambda
    DynamoDB
    1. 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. 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. 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. 4

      Tallies read back

      That same Lambda function reads the fresh totals for every option back out of DynamoDB, ready to send.

    5. 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.