DiceDecide

How to Add a Fair Coin Flip to a Website Without Getting the Odds Wrong

Be the first to rate this page.

A coin flip added to a web page needs one fair bit: an outcome chosen with exactly 50% probability from a uniform random source, mapped once to Heads or Tails before any animation runs. Reusing an existing tested tool through an iframe is simpler and safer than writing a new random-number implementation from scratch.

A worked example with numbers

A common approach is `Math.random() < 0.5`. MDN documents Math.random() as returning a pseudo-random float uniformly distributed over the half-open interval from 0 inclusive to 1 exclusive, so a 0.5 threshold splits that continuous interval into two equal-length halves — for a binary choice this is unlike the range-conversion bias that affects picking among three or more items, because there is no remainder or endpoint to distort. `Math.round(Math.random())` also lands on exactly two values, 0 or 1, split the same way, since JavaScript’s round-half-up rule sends the single boundary value 0.5 to 1 without creating an uneven interval — a rare case where a rounding pattern that is wrong for wider ranges happens to be correct for exactly two outcomes. That special case does not generalise: rounding a scaled random value to choose among three or more labelled outcomes gives the two endpoint values roughly half the selection width of the interior values, the range-conversion bias covered in a related guide on this site — the coin-flip case stays safe only because it has exactly one boundary, at 0.5, splitting the interval into two equal pieces.

How to set the rule before the result

For a page that must not depend on a third-party iframe, implement the bit locally with the Web Crypto API: `crypto.getRandomValues(new Uint32Array(1))[0] & 1` draws one uniformly distributed 32-bit integer and keeps only its lowest bit. Because exactly half of all possible 32-bit values are even and half are odd, that final bit is exactly 50/50, with no rejection step needed — unlike converting a wider integer into a range of three or more outcomes. Map the bit to Heads and Tails once, then play the animation; never let the animation itself pick which face to end on. Test the implementation by temporarily logging a large number of results to the browser console and counting Heads versus Tails; a genuinely fair implementation should land close to an even split over a few thousand simulated calls, while a coded-in bug — such as an off-by-one in the bitmask or an accidentally reused seed — usually shows up as a visibly skewed count well before that. Keep the test script separate from the shipped page: a temporary console loop that runs 10,000 simulated flips is a fast, cheap check during development, and there is no reason for that loop to remain in the code a visitor actually downloads. Removing debug-only randomness testing from the shipped bundle also avoids shipping extra script weight for a check that only matters once, during development, rather than on every page load a real visitor triggers.

Common mistakes that change the odds or the process

A frequent implementation mistake is seeding the “random” outcome from something predictable, such as the current time or a page-load counter, which is not random at all and can be identical for two visitors who load the page in the same instant. Another is generating the result only after the animation finishes, based on where a CSS transition happens to stop — that couples the outcome to rendering performance, which varies by device and is not a controlled probability at all. A third is silently falling back to Math.random() without disclosing it when getRandomValues is unavailable, leaving a page that claims cryptographic strength it does not have. A fourth mistake is treating a plausible-looking animation as evidence that the underlying selection was fair: a coin can spin realistically for two seconds and still land on a result chosen by broken, biased, or entirely fake logic underneath, because the visual layer and the selection logic are implemented separately and neither one can verify the other from a screenshot alone.

Where this method stops being appropriate

None of this makes client-side code trustworthy for a real wager: any script running in a visitor’s own browser can be edited in developer tools before a result is acted on, so a coin flip embedded this way is appropriate for a game, poll, or icebreaker, not for settling money between parties who do not trust each other. A server-generated and server-recorded result is required whenever a flip has to be provably unaltered after the fact. This limit is specific to a decision one party could gain from manipulating; it does not mean client-side randomness is unsuitable for its far more common use — a game, a poll, an icebreaker, or any decision where every participant would accept either outcome without a financial or competitive stake in the result.

How the random source fits into the rule

MDN’s Math.random reference documents Math.random()’s uniform interval and states it is not suitable for cryptographic purposes; MDN’s Crypto.getRandomValues reference documents getRandomValues() as the browser’s cryptographically strong alternative for exactly this kind of implementation. Both references describe interfaces implemented by the browser itself rather than by any individual website, so their guarantees hold the same way regardless of which page’s script happens to call them.

Why client-side code cannot settle a real wager

Every part of a page’s JavaScript runs inside the visitor’s own browser, which means the visitor controls it: opening developer tools and editing a variable, pausing execution, or replaying a network response are all available to anyone who wants a specific outcome. That is a fine trade-off for a low-stakes decision where nobody has a strong incentive to cheat, but it rules out client-side code for a bet, a dispute resolution, or any situation where one participant benefits from forcing Heads or Tails. Settling a real wager needs a result generated and logged on a server neither party controls, not a page they can inspect and modify.

The same limit applies to an embedded iframe pointing at someone else’s tool: the embedding page still cannot prove to a sceptical counterparty that the iframe’s content was not swapped, blocked by an extension, or replaced before the result was read. A server-side log with a timestamp and a shared, tamper-evident record — the same requirement that makes a network true-random API attractive for high-stakes uses — is the only way to remove the visitor’s own browser from the trust chain entirely.

A minimal fair-bit snippet, explained line by line

Reserve space for one 32-bit integer, fill it with a cryptographically strong random value from crypto.getRandomValues(), then keep only its least-significant bit with a bitwise AND against 1. Because a uniformly distributed 32-bit integer has exactly 2,147,483,648 even values and 2,147,483,648 odd values, that final bit is 0 or 1 with exactly equal probability — no rejection loop is needed here, unlike the range-of-N problem this site’s list and dice selectors solve.

The same pattern extends to flipping several coins in one call: draw one 32-bit integer, and read as many low bits as needed, one per coin, since each bit position in a uniformly random integer is itself uniformly 0 or 1 and independent of the others. Reading five bits this way is equivalent to five separate fair flips, without five separate calls to the underlying random source.

Before publishing an embedded coin flip, it is worth writing down, even briefly, what source generates the bit, what happens on an unsupported browser, and what the widget is and is not meant to be used for. That short disclosure does more for a visitor’s ability to trust the widget than any additional animation polish, because it lets a technically minded visitor verify the claim instead of having to take it on faith.

Why a coin is safe from a bias that a bigger wheel is not

Converting a raw random integer into exactly two labelled outcomes by keeping its lowest bit is always exactly fair, because any power of two — including the 2³² possible values of a 32-bit integer — splits into precisely equal halves of even and odd values. The moment a widget needs three, six, or seven outcomes instead of two, that guarantee disappears unless the code adds a rejection step.

MethodSource rangeLeast-common outcomeMost-common outcome
getRandomValues() & 1 (2 outcomes)2³²50.000000%50.000000%
1-byte value % 7 (7 outcomes)25614.0625% (results 4–6)14.4531% (results 0–3)
32-bit value % 3 (3 outcomes)2³²33.333333326%33.333333349%

The 32-bit-modulo-3 bias is far too small to ever matter in practice — about two parts in ten billion — because 2³² is huge relative to 3. The 1-byte-modulo-7 bias is a different story: with only 256 possible source values, results 4 through 6 land about 0.39 percentage points less often than results 0 through 3, a gap large enough to show up in a modest simulated test run. The lesson is not that modulo is always broken — it is that the size of the bias depends entirely on how evenly the source range divides by the number of outcomes, which is exactly why the two-outcome coin case is the one situation where a beginner’s naive approach happens to be safe by construction.

A short test run that looks broken but is not

Suppose a developer manually tests a new coin-flip function by running it 20 times and counts 8 heads and 12 tails — a 40/60 split that looks alarming at first glance. It is not: the probability of exactly 8 heads in 20 fair flips is about 12.01%, and the probability of a split at least that lopsided in either direction — 8 or fewer heads, or 12 or more — is about 50.34%, meaning a genuinely fair implementation produces a split this uneven or worse more than half the time over repeated 20-flip test runs. Concluding “this is biased” from one 20-flip manual test, and rewriting working code in response, is a common way to introduce a real bug while chasing an imaginary one.

How many logged flips actually prove something

Testing a coin-flip implementation for a 1-percentage-point bias with 95% confidence needs the same sample-size arithmetic used throughout this site: n = 1.96² × 0.25 ÷ 0.01² ≈ 9,604 logged flips. A temporary console loop that calls the function 10,000 times and tallies the results, run once during development, comfortably clears that bar and is a reasonable sanity check; a manual click-count of a few dozen flips, however suspicious the split looks, is not, and should never be used to justify shipping a “fix” for code that was working correctly.

What a passing test run does not prove

Even a clean 10,000-flip test result only shows that the tested code, run in that browser, on that day, produced a balanced split. It says nothing about whether the exact same code is what actually ships to a visitor — a build step, a bundler misconfiguration, or a stale cached script could serve a different implementation than the one tested, and no statistical property of the output can detect that mismatch. Confirming what actually shipped requires checking the deployed bundle directly, an engineering discipline that sits entirely outside what any amount of flip counting can verify.

Related DiceDecide tools

Enter your values, review the result, then use it with confidence.

Rate this page

Be the first to rate this page.