Uttir
By Uttir 6 min read

How to Pick a Random Number Fairly (and Why Math.random Is Wrong)

A practical guide to random numbers: the difference between Math.random and crypto.getRandomValues, why fairness matters for giveaways and raffles, how to roll dice and shuffle a list in your browser, and the one case where randomness is genuinely hard.

For a giveaway, a raffle, a coin flip, or a quick "pick a number from 1 to 100", the right tool is the browser's built-in cryptographically secure RNG (crypto.getRandomValues). It is unbiased, unpredictable, and fast. For most uses, you do not need the cryptographic part — but using it costs nothing extra, and it removes any doubt about fairness. A browser-based random number generator rolls the dice in your browser, and the result is reproducible only by the person running the tool. For cryptographic keys, nonces, and password salts, the same API is what every TLS connection uses.

Random numbers are one of those things that look trivial — "pick a number from 1 to 100" — but get interesting fast. The wrong choice of randomness can make a giveaway predictable, a raffle riggable, a password crackable, or a statistical test fail. This post covers the difference between weak and strong randomness, when each is appropriate, how to roll dice and shuffle lists fairly in your browser, and the one case where getting randomness right is genuinely hard.

Math.random vs. crypto.getRandomValues

Most programming languages ship a "random" function that is fast, convenient, and not actually random in any cryptographic sense. In JavaScript, that function is Math.random. It is a pseudo-random number generator (PRNG): a deterministic algorithm that produces a sequence of numbers that look random but are entirely predictable if you know the seed. Most implementations use a 64-bit internal state, which means an attacker who has seen enough of the sequence can reconstruct the seed and predict every future value.

For most uses — games, animations, picking a background color, shuffling a playlist — Math.random is fine. The values look random, they pass basic statistical tests, and the consequences of an attacker guessing them are nil.

For other uses — generating a password, picking a lottery winner that needs to be auditable, generating a cryptographic key, rolling a nonce that must be unique — Math.random is the wrong choice. The right choice is the browser's crypto.getRandomValues, which is a cryptographically secure random number generator (CSPRNG). It draws from the operating system's entropy pool (which is fed by unpredictable physical processes: mouse movements, keyboard timings, disk activity, hardware RNG on modern CPUs), and the output passes the statistical tests that distinguish CSPRNGs from PRNGs.

The Random Number Generator on this site uses crypto.getRandomValues for all draws. The cost is essentially zero (the same API is used for every TLS connection), and the result is unpredictable and unbiased.

Unbiased means uniform, not "no pattern"

A common source of bugs is the modulo bias. If you want a random number from 0 to 99 and you call crypto.getRandomValues(new Uint8Array(1))[0] % 100, the result is biased toward the lower numbers — because 256 is not evenly divisible by 100, the values 0-55 have a slightly higher chance of appearing than 56-99.

The fix is the rejection method: ask for random bytes, and reject any value that would cause the bias. For 0-99, ask for a random byte. If the byte is 200 or higher (200-255 = 56 values), reject it and try again. The accepted values are 0-199, which is 200 values evenly divisible by 100. Each of the 100 possible results gets exactly 2 of the 200 values. No bias.

The Random Number Generator implements the rejection method for every range. The math is invisible to the user, but the result is correctly uniform.

Common cases that need a fair draw

Giveaway winner

"Pick a random number from 1 to N where N is the number of entries." The tool that does this well uses a CSPRNG and the rejection method, and shows the user the seed or hash that produced the result so the draw is auditable. The browser-based tool is auditable in a different way: the source code is visible, the random bytes come from the browser's own CSPRNG, and the result can be verified by anyone who runs the same tool with the same input. For most giveaways, this level of auditability is more than enough.

Raffle and lottery

Same as a giveaway, but the stakes are usually higher. The right approach is the same: CSPRNG, rejection method, public source code. The legal requirements for lotteries vary by jurisdiction, but the technical approach is the same.

Coin flip

"Heads or tails" is a random draw from a set of 2 outcomes. A single random byte, modulo 2, would have a slight bias (since 256 is not even). The fix is the same: rejection method, or just take the high bit of a random byte (which is exactly 50/50 because 128 is even). The Random Number Generator includes a "coin flip" preset that does this.

Dice roll

"Roll a six-sided die" is a random draw from 1 to 6. The same approach: rejection method on a random byte, accepting values 0-251 (252 is divisible by 6), then mapping to 1-6. The Random Number Generator includes a "dice" preset for one die and for multiple dice (3d6, 4d6, etc.).

Shuffling a list

The Fisher-Yates shuffle is the standard. Walk the list from the end to the beginning; at each position, swap the current item with a randomly chosen item from the positions 0 to current. The result is an unbiased permutation of the original list, assuming the random choices are unbiased. The Random Number Generator's "shuffle" feature implements Fisher-Yates with the rejection method for the random choices.

Password or token

A 16-character random password is 16 draws from a 94-character pool (printable ASCII). A 256-bit API token is 32 draws from a 256-value pool (random bytes). For both, the CSPRNG is the right choice. The Password Generator uses crypto.getRandomValues internally for the same reason.

When strong randomness matters

The use cases where crypto.getRandomValues is required, and Math.random is a real vulnerability:

  • Passwords and passphrases. A predictable password is not a password. The Password Generator uses CSPRNG; any generator that uses Math.random is a security risk.
  • API keys and session tokens. A predictable token is a session-takeover. The CSPRNG is the only acceptable source.
  • Cryptographic nonces. A reusable nonce breaks the encryption. CSPRNG.
  • Password reset tokens. Same as API keys. CSPRNG.
  • UUIDs. UUIDs are 128-bit values where uniqueness is the security property. Using Math.random would give unique-looking values that are predictable, which is bad for security tokens but fine for non-security identifiers. The UUID Generator uses CSPRNG.

When Math.random is fine

The use cases where predictability is not a problem and Math.random is acceptable:

  • Visual effects. Animation timings, particle effects, randomly chosen background colors, the "surprise me" feature in a tool that picks a default option.
  • Non-security sampling. "Pick a random quote to show" or "pick a random testimonial to feature". The quotes are already public; the only property needed is "looks random".
  • Game logic (single-player). "Roll a die in a single-player game against the computer." The computer is not trying to cheat, so the source of randomness does not matter.
  • Statistical simulations. Monte Carlo methods, simulations, sampling from a distribution. For most research-grade work, even Math.random is fine; for cryptographic or financial simulations, the CSPRNG is required.

The one case where randomness is genuinely hard

Cryptographic key generation, specifically, requires more than just a CSPRNG. The browser's crypto.getRandomValues is suitable for session tokens, nonces, and password salts, but the generated key is only as good as the entropy that went into the CSPRNG. For a high-stakes key (a long-term encryption key, a CA signing key), the right answer is a hardware security module (HSM) or a dedicated hardware RNG (TPM, YubiKey) that produces entropy from a trusted source.

For personal use — encrypting your own files, your own notes, your own messages — the browser's CSPRNG is fine. The threat model is "someone with access to my computer", not "a nation-state attacker with physical access to my CPU's entropy source". For the personal threat model, the browser is the right tool.

How to use the random number generator

  1. Open the Random Number Generator.
  2. Set the range (min and max, inclusive).
  3. Choose how many numbers to draw (one for a single pick, many for a raffle).
  4. Or pick a preset: coin flip, dice roll, shuffle a list.
  5. The result appears instantly, with the option to copy or re-roll.

For the shuffle preset, paste a list (one item per line) and the tool returns the same items in a random order. The result uses Fisher-Yates with the rejection method, so every permutation is equally likely.

Common mistakes to avoid

  1. Using Math.random for a security-sensitive draw. The draw may look fair but is predictable to an attacker who has seen enough of the sequence.
  2. Using a single random byte for a multi-value choice without rejection. The result is biased toward the lower values. The fix is the rejection method.
  3. Picking a "random" number from your head. Human randomness is heavily biased (people pick 7, 17, 23, 42 more often than other numbers in 1-100). The right answer is a CSPRNG, not a gut feeling.
  4. Re-using the same seed for multiple draws. If two draws use the same seed, the results are correlated. CSPRNGs are designed to avoid this; PRNGs with a fixed seed are not.
  5. Not considering reproducibility. For some uses (debugging a simulation, replaying a game), reproducibility matters. The CSPRNG is not reproducible; if you need a reproducible "random" sequence, you want a seeded PRNG. For the personal-use cases (giveaway, raffle, password), reproducibility is a feature, not a bug — you want each draw to be unique.

The Random Number Generator handles all of these correctly. The CSPRNG is the source, the rejection method is the unbiased-mapping function, the Fisher-Yates is the shuffle, and the results are not reproducible across calls. For most uses, this is exactly the right behavior.

#random#cryptography#raffle#dice#shuffle#security

New tools and guides, once a week

One short email when something new ships. No tracking, no images, unsubscribe with one click.