Fair Random Decisions: Coin Flips, Wheels, Shuffles and Even Groups
How browser randomness works, why sort(() => Math.random() - 0.5) is biased, what 350,757 real coin flips revealed, and how to draw names and split teams fairly.
· 9 min read

Paste [1, 2, 3].sort(() => Math.random() - 0.5) into a Node.js 25 console 600,000 times and count the results. Two of the six possible orders, 1,2,3 and 2,1,3, each come up about 25% of the time. The other four come up about 12.5% each. A one-liner that half the internet uses to "shuffle" an array makes some outcomes twice as likely as others, and nobody notices because any single run looks random.
Fairness in a random decision is not about how random a single result looks. It is about whether every outcome had the same chance before you pressed the button. Below is how browsers produce random numbers, the three bugs that quietly break fairness, and how each of the decision tools on this site avoids them: the Coin Toss, Spin Wheel, Team Lunch Decider, Drum Roll Decider, Group Splitter and Random Picker.
Where browser randomness comes from
JavaScript gives you two sources.
| Source | What it is | Good for |
|---|---|---|
Math.random() | A fast pseudo-random generator seeded once per tab (xorshift128+ in V8 since 2015) | Animations, confetti, game effects |
crypto.getRandomValues() | Bytes from the operating system's cryptographically secure generator | Anything where someone might care about the result: draws, tokens, passwords |
Math.random() is not broken. Its output is statistically fine for most purposes, but its internal state is small and recoverable from a handful of outputs, and the spec makes no promise about quality. crypto.getRandomValues() fills a typed array (up to 65,536 bytes per call) from the same source the browser uses for TLS keys. All six decision tools here draw winners from crypto.getRandomValues(). Confetti positions still use Math.random(), because nobody audits confetti.
A random source gives you bits. A decision needs "a whole number from 0 to n minus 1, each equally likely". Getting from one to the other is where the first bug lives.
Bug one: modulo bias
The obvious conversion is randomByte % n. Take one random byte (0 to 255) and reduce it mod 6 to roll a die. 256 is 6 times 42 plus 4, so the values 0, 1, 2 and 3 each have 43 ways to appear and 4 and 5 have only 42. A 2.4% relative tilt toward low numbers.
With 32-bit values the tilt shrinks, but it never reaches zero unless n divides 2^32. The fix is rejection sampling: compute the largest multiple of n that fits, and if the draw lands above it, throw it away and draw again.
function randomIndex(n) {
const limit = Math.floor(2 ** 32 / n) * n; // largest multiple of n below 2^32
const buf = new Uint32Array(1);
let x;
do { crypto.getRandomValues(buf); x = buf[0]; } while (x >= limit);
return x % n;
}
For n = 6 the rejected zone is 4 values out of about 4.3 billion, so the loop almost never repeats. The Random Number Generator uses the same approach for arbitrary ranges, and the decision tools reuse it.
Bug two: shuffles that are not uniform
A shuffle of n items is fair when each of the n! orderings is equally likely. Two popular shortcuts fail that test.
The sort trick. sort(() => Math.random() - 0.5) asks a sorting algorithm to work with a comparator that lies. The result depends on which comparisons the engine makes. For three items V8 does two or three comparisons, each a coin flip, so the outcomes can only be multiples of 1/8, and 1/6 is not one of them. That is where the 25% and 12.5% numbers above come from.
The naive swap. Swapping every position with a random position anywhere in the array looks reasonable but produces n^n equally likely swap sequences. For three items that is 27 sequences spread across 6 orderings. 27 is not divisible by 6, so some orders get 5 sequences (18.5%) and others get 4 (14.8%).
Fisher-Yates fixes both. Ronald Fisher and Frank Yates described it for pencil and paper in 1938, and Richard Durstenfeld published the in-place computer version in 1964. Walk from the last position down, and swap each one with a random position at or below it:
for (let i = a.length - 1; i > 0; i--) {
const j = randomIndex(i + 1); // 0..i inclusive
[a[i], a[j]] = [a[j], a[i]];
}
The number of possible swap sequences is n times (n minus 1) times ... times 2, which is exactly n!, one per ordering. The Group Splitter, the Random Picker's Full Random Order mode and the shuffle animations in the lunch and drum roll deciders all use this loop.
Coin Toss: the fairest coin is not a real one
A real coin is close to fair but not exactly. In 2007 Persi Diaconis, Susan Holmes and Richard Montgomery analysed the physics of a flipped coin and predicted that, because it wobbles in the air, it lands on the side it started on about 51% of the time. In 2023 František Bartoš and colleagues put that to the test with 350,757 flips by 48 people. They measured a same-side rate of about 50.8%, with noticeable variation between flippers. They found no meaningful difference between heads and tails as such: the bias is toward whichever face was up at the start.
The Coin Toss skips the physics. Each flip calls randomIndex(2), so heads and tails are exactly one half each and independent of the last flip. Its Statistics card is a useful reality check on intuition: in 10 flips, a split of 7 to 3 or worse happens about 34% of the time with a perfect coin. Only after a few hundred flips do the percentages sit close to 50.
Spin Wheel: the slice under the pointer must be the winner
A wheel can use perfect randomness and still be wrong, if the slice it announces is not the slice the pointer is over. The usual cause is mixing angle conventions. On an HTML canvas, angle 0 points to 3 o'clock and angles grow clockwise, because the y axis points down. People tend to think in compass terms, with 0 at 12 o'clock.
The Spin Wheel keeps its red arrow at 3 o'clock, angle 0, and draws slice i from i × slice + rotation to (i + 1) × slice + rotation. The winner is whichever slice contains angle 0 after the spin:
normalised = rotation mod 360
winner = floor((360 - normalised) / (360 / n)) mod n
With four slices and a final rotation of 100 degrees, (360 minus 100) / 90 = 2.9, so slice 2 is under the arrow. The 8 to 12 extra turns and the slow ease-out only change how long the spin feels; they add whole circles, which do not change the result. Fairness then comes from one thing, a final angle drawn uniformly over the circle, which gives each equal slice a 1 in n chance. Want an option weighted double? Put it on two lines.
Team Lunch Decider and Drum Roll Decider: keep the theatre separate
Both of these tools add suspense: a two second drum roll, then names shuffling on screen every 100 milliseconds, for a random 10 to 20 seconds in total. The Team Lunch Decider is set up for restaurants and the Drum Roll Decider for any list (it used to be called Drum Rolls Please).
The design choice that matters is that the animation never picks the winner. When the timer ends, the tool makes one separate draw, options[randomIndex(options.length)]. Stopping on "whatever name is on screen when the timer fires" can also be fair, but it ties the result to frame timing and render speed, which is much harder to reason about or test.
Both tools reject exact duplicate entries, so nobody can type their favourite twice. If you want to give an option extra weight on purpose, the Spin Wheel and Random Picker allow repeated lines.
A good group ritual: everyone names one place and vetoes one, then the drum roll picks from what survives. The group shaped the list, so nobody fights the result.
Group Splitter: balanced sizes, then a fair shuffle
Splitting people into teams has a second fairness question besides randomness: how big each group is. Take 10 people and ask for groups of 3. The naive split gives 3, 3, 3 and 1, and the one person on their own will not thank you. Ask for 4 groups from 9 people with block sizes of ceil(9 / 4) = 3, and you get only three groups.
The Group Splitter computes the group count first, then shares out the remainder:
| Names | Request | Groups made | Sizes |
|---|---|---|---|
| 10 | Group size 3 | ceil(10 / 3) = 4 | 3, 3, 2, 2 |
| 9 | 4 groups | 4 | 3, 2, 2, 2 |
| 15 | Group size 4 | 4 | 4, 4, 4, 3 |
| 7 | 3 groups | 3 | 3, 2, 2 |
No two groups differ by more than one person, and in Group Size mode no group goes over the size you asked for. Random assignment shuffles with Fisher-Yates and then deals the balanced sizes. Round Robin deals names in list order like cards, which is the move when your list is sorted by skill or team and you want each group to get a spread. Block keeps neighbours together.
Random Picker: with or without replacement
The Random Picker is the general tool, and its key setting is Allow Duplicates.
- Off (without replacement): each draw removes the winner from the pool. Drawing 3 from 10 gives 10 × 9 × 8 = 720 equally likely ordered results and nobody wins twice. This is what you want for raffles and turn orders.
- On (with replacement): every draw uses the whole list. Drawing 3 from 10 has 1,000 outcomes, and a 28% chance that someone comes up twice. This fits daily rotations like "who buys coffee today".
Duplicate lines are separate tickets. Enter "Ana" twice and "Ben" once and Ana wins the first draw two times in three; if she wins, only one copy of her name is removed. The Raffle Tickets button generates a numbered range (for example 1 to 100) so you can run a numbered draw, and Download saves the winners in order with timestamps.
Mistakes that make a fair draw look unfair
- Re-rolling until you like the result. Every re-roll is a new draw, so "best of whatever it takes" is not random any more. Agree on the stakes before pressing the button.
- Reading meaning into streaks. Five heads in a row has a 1 in 32 chance from any starting point. In 100 flips, a run of six or more is normal.
- Editing the list mid-session. In the Random Picker, change the list and press Reset, or the remaining pool still reflects the old list.
- Assuming a shared link is private. A shared wheel carries its whole list in the URL.
- No record for a real prize. The randomness is sound, but nobody else can verify it after the fact. For anything with money attached, draw on a shared screen and keep the downloaded results.
For a quick two-way call, flip the coin. For a visible pick in front of a room, spin the wheel or run the drum roll. For several winners, a full order, or a raffle, use the Random Picker with duplicates off, and for teams, let the Group Splitter balance the sizes for you. If the choice actually has trade-offs to weigh, a random draw is the wrong tool; score it in the Decision Maker or read the weighted decision matrix guide instead.