Random numbers, seeds and when not to use them
Why most random numbers are not random, when that matters and when it does not, and the modulo bias that quietly skews a naive range.
Last reviewed · 1,302 words
In short
- Most software randomness is pseudo-random — deterministic output from a seed that merely looks unpredictable.
- For anything security-related, use a cryptographic source. Math.random is not one and never has been.
- Taking a remainder to fit a range introduces modulo bias, making some values slightly more likely than others.
- A seeded generator reproduces the same sequence every time, which is a feature for testing and a flaw for a lottery.
- Humans are poor at both generating and recognising randomness, which is why genuine random sequences look wrong.
There are two kinds of random number generator and the difference decides whether a system is secure.
Pseudo-random against true random
A pseudo-random number generator (PRNG) is an algorithm. Given a starting value — the seed — it produces a sequence that passes statistical tests for randomness and is completely determined. The same seed always produces the same sequence.
Mersenne Twister, xorshift and PCG are common ones. They are fast, produce good statistical distributions, and are entirely predictable to anyone who knows the algorithm and enough of the output.
A cryptographically secure PRNG (CSPRNG) is also an algorithm, designed so that observing any amount of past output gives no useful information about future output. It is seeded from operating system entropy — timing jitter, hardware noise, interrupt patterns.
A true random generator samples a physical process: thermal noise, radioactive decay, atmospheric noise. Slower, and used to seed the others.
Which to use
Use a cryptographic source for passwords, tokens, session identifiers, encryption keys, password reset links, anything a lottery pays out on, and anything an adversary would benefit from predicting.
In a browser that is crypto.getRandomValues(). In Node it is the crypto module. In Python it is the secrets module, not random.
A general-purpose PRNG is fine for simulations, shuffling a playlist, procedural generation, sampling for analysis, and games where nobody is trying to cheat.
Math.random() is not cryptographic. It has never been specified as such and browsers implement it with a fast non-cryptographic algorithm. Using it to generate a password reset token is a real and recurring vulnerability, and it has been exploited.
The cost of using the secure source is negligible for the volumes any ordinary application needs. Where the two differ in speed by enough to matter — a Monte Carlo simulation drawing billions of values — that is exactly the case where security does not apply.
Modulo bias
The bug that appears in almost every hand-rolled range function.
To get a number from 0 to 9 from a generator producing 0 to 255, the obvious approach is value % 10.
256 does not divide evenly by 10. Values 0 to 5 can each be produced by 26 of the 256 inputs; values 6 to 9 by only 25. So 0 through 5 are 4% more likely than 6 through 9.
At this scale the skew is small. Reducing a 32-bit value to a range near 2³¹ makes some values nearly twice as likely as others.
The correct approach is rejection sampling: compute the largest multiple of the range that fits, discard any draw above it, and try again. It costs a small number of extra draws and removes the bias entirely.
crypto.getRandomValues() does not fix this on its own — the bias is introduced by how you reduce the value, not by the source. Language-provided range functions such as Python's secrets.randbelow() handle it correctly, which is a reason to use them rather than writing the reduction yourself.
Seeds and reproducibility
A seeded generator produces the same sequence every time, which is either exactly what you want or a serious flaw.
Where it is a feature: reproducible tests, scientific results others must be able to replicate, procedural generation where a world must regenerate identically from a share code, and debugging a failure that depends on random input.
Where it is a flaw: anything secret. Seeding from the current time is the classic error — an attacker who knows roughly when a token was generated can search a few thousand candidate seeds and reproduce it exactly. This has broken real systems, including online poker sites and session token schemes.
The rule: if the sequence should be reproducible, seed it deliberately and record the seed. If it should be unpredictable, do not seed it at all — let the cryptographic source handle it.
Humans are bad at randomness
Two related failures, both well documented.
Generating. Asked to produce a random sequence, people avoid repetition. They will not write the same number twice in a row, they alternate more than chance would, and they distribute values too evenly. A genuine sequence of coin flips contains runs of five or six heads and looks wrong to almost everyone.
Recognising. Because true randomness clusters, people see patterns in it. This is the gambler's fallacy — believing that after five reds, black is due. It is not; the wheel has no memory and each spin is independent.
The commercial consequence is worth knowing: music streaming services found that genuine shuffle produced complaints about "not being random", because the same artist appeared twice in a row. Several now use a deliberately non-random algorithm that spaces artists out, precisely because it feels more random than random does.
The birthday problem
Random identifiers collide more often than intuition suggests.
With 23 people, the chance that two share a birthday is above 50%. The reason is that there are 253 pairs, not 23 comparisons.
The general result: collisions become likely at around the square root of the space size. A random 4-digit code has 10,000 possibilities, so a collision is likely after roughly 100 codes. A 32-bit identifier collides after about 65,000.
This is why UUIDs are 128 bits. The space is large enough that collisions remain negligible at any realistic volume — generating a billion per second for a century leaves the probability vanishingly small.
Anyone generating short random codes — order references, coupon codes, short URLs — needs either to check for collisions or to make the code long enough that they cannot happen. Assuming randomness prevents duplicates is how a booking system ends up with two orders sharing a reference.
Shuffling correctly
Randomising the order of a list is a place where the obvious approach is wrong.
array.sort(() => Math.random() - 0.5) appears in a great deal of code and does
not produce a uniform shuffle. Sort algorithms assume the comparison function is
consistent, and an inconsistent one produces a distribution that is measurably
skewed — with some orderings several times more likely than others, depending on
the engine's sort implementation.
The correct method is the Fisher-Yates shuffle: walk the array from the end, and for each position swap it with a random earlier position, inclusive of itself. It is a few lines, it runs in linear time, and every permutation is equally likely.
The subtle version of the same bug is choosing the random index from the whole array rather than from the remaining unshuffled portion, which produces a biased result that looks fine in casual testing. The distinction matters wherever the shuffle decides something — a draw, a randomised trial, a card game.
Sampling without replacement
Drawing distinct values — lottery numbers, a random subset of rows, a team assignment — is a different operation from drawing repeatedly.
Repeated draws produce duplicates. Rejecting duplicates works and becomes slow as the sample approaches the population size: drawing 99 distinct values from 100 spends most of its time rejecting.
The efficient approaches are a partial Fisher-Yates shuffle, which stops after the required number of positions, or reservoir sampling where the population is too large to hold in memory or arrives as a stream.
For the common case — a handful of values from a modest range — either works, and the thing worth avoiding is the loop that draws and rejects without a bound on how long it will run.
What this tool assumes
- Numbers are drawn from the browser's cryptographic random source rather than a general-purpose generator.
- Range reduction uses rejection sampling, so every value in the range is equally likely with no modulo bias.
- Where unique values are requested, the result is a sample without replacement rather than repeated draws.
- Nothing is transmitted or logged. Generation happens entirely in your browser.
- For anything with money or security attached, verify the source rather than trusting any web page — including this one.