🎡 SpinForFree

How the Live Spin Actually Works

August 2, 2026

Open a wheel’s share link on three different devices and spin it once. All three land on the same name at the same instant, with no refresh needed and nobody seeing a different result. That sounds like it ought to be the obvious way to build a shared wheel, and it is not how most of them work. Here is what actually happens between pressing Spin and that moment, and why each step is where it is.

The problem with the obvious design

The straightforward way to build a spinning wheel is to do everything in the browser: pick a random slice in JavaScript, animate to it, announce the name. It is less code, it needs no server, and it works perfectly for one person.

It falls apart as soon as a second person is watching. Two browsers running their own random pick produce two different winners, so there is no shared event to witness, only a collection of private ones. And because the result is generated fresh on every page load, refreshing is a free reroll. An organiser can spin until they like the answer and screenshot only that. The generator might be flawless; the process has no integrity at all.

Fixing that means moving one specific thing out of the browser: the decision.

1. The owner presses Spin

The browser sends a single request with no parameters in it. Not “spin wheel 7”, not “pick from these names”, nothing. The server already knows which wheel belongs to that session, and it reads the current participant list from the database itself.

That emptiness is deliberate. A request that carries no inputs has no inputs to tamper with. There is no name list to substitute, no seed to nudge, no weight to adjust in flight. The request is a trigger and nothing else.

2. The server draws the winner

The draw is a weighted ticket selection. Every active participant’s weight is summed, a random integer is drawn in that range, and the code walks the list subtracting weights until it goes negative. The participant it stops on is the winner. A name with a weight of 3 in a pool totalling 10 holds three of the ten tickets, and wins exactly 30% of the time.

The random integer comes from the Web Crypto API, not Math.random, and it is drawn with rejection sampling: values landing in the ragged tail of the 32-bit range are discarded and redrawn rather than folded in with a remainder. Without that step the participants at the start of the list would be very slightly more likely to win, a bias far too small to observe and pointless to accept when avoiding it costs three lines. The full explanation of modulo bias is here.

Then, in a single database transaction: the result is written to the draw history, the winner’s win counter is incremented, and elimination is applied if it is switched on. All of this completes before anything at all is sent back to any browser, including the owner’s own.

That ordering is the entire fairness claim. By the time any screen could possibly interfere, the outcome is already recorded. There is nothing left to influence.

3. One instruction, broadcast to everyone

Here is the part that makes the shared spin work. The server does not tell each screen “the winner is Priya” and leave every browser to animate however it likes. It publishes one instruction describing the whole animation:

  • which participant won
  • the jitter, an offset within the winning slice between -0.5 and 0.5, so the pointer does not stop dead centre every time
  • how many full turns to make before settling
  • how long the spin lasts

Every screen receives that same instruction. The jitter and turn count are the interesting entries. If each browser generated its own, the wheels would all reach the correct name but along visibly different paths and at different moments. Sending them from the server means every wheel traces the identical rotation and stops at the identical resting angle. No browser is deciding anything; each is playing back instructions they all received at once.

4. Joining mid-spin

Open the link while a spin is already running and you do not miss it. The server keeps the in-flight spin and, on connection, hands the joiner a rescaled version of it: the same winner and jitter, but with the duration cut to the time actually remaining and the turn count reduced to one.

So somebody arriving two seconds into a five-second spin sees a three-second spin that lands on the correct name at the correct moment, in step with everyone else. Arrive within the last quarter-second and you get nothing, because there is no point starting an animation that cannot finish.

A double wheel, where a name is drawn and then a second wheel spins for the prize, rescales each phase separately. Join during the name phase and you catch the tail of it, then see the prize phase in full. Join during the prize phase and the name phase collapses to instant so your screen catches up before the prize wheel finishes.

How the connection stays alive

The live channel is a server-sent-events stream, which is a single long-lived HTTP response the server writes to as things happen. Simpler than a WebSocket and a better fit, because the traffic is almost entirely one-directional: the server has things to announce and viewers have nothing to say back.

It is kept alive with a periodic ping, which is not optional. Proxies and load balancers sitting between the server and a viewer will close a connection that has been silent for too long, and a wheel that has not spun in ten minutes is exactly that. The ping keeps the pipe visibly in use.

The same stream carries two other things: a live viewer count, updated as people arrive and leave, and a nudge whenever the owner changes names or settings, so everyone watching is looking at the current wheel rather than the one that existed when they opened the page.

An honest limitation

The pub/sub layer that holds these connections lives in the application process. One process holds every open stream, which is exactly enough for a single-instance deployment and is what this runs on.

It would not survive being scaled to several replicas without a change. A spin triggered on one instance would only reach viewers connected to that same instance, and everyone else would see nothing. The fix is a shared broker, Redis pub/sub or equivalent, in place of the in-process emitter, with the rest of the design unchanged. Worth stating plainly rather than implying the architecture scales further than it does.

Why build it this way

Every decision above follows from one rule: the result is decided in exactly one place, once, and written down before anyone can see it. The empty request, the server-side draw, the transaction, the broadcast jitter. Each exists to remove a way the outcome could differ between screens or be influenced after the fact.

What that buys is not a better random number. It is a draw that leaves evidence. See why every draw gets logged for what gets kept and why it matters, or the random name picker guide for how this compares with simpler pickers.

Advertisement