Press a button, a number appears, and it is assumed to be random. The question worth asking is what produced it — because one of the two answers is verifiable and the other is not.
Where Genuine Randomness Comes From
True randomness comes from physical processes that are not determined by prior state:
- Radioactive decay — the exact moment an atom decays cannot be predicted
- Thermal noise — random voltage fluctuations in a resistor
- Atmospheric noise — the background static of lightning activity
- Quantum measurement — outcomes that are genuinely indeterminate
These are not approximations of randomness. They are random, in the sense that no amount of knowledge about the past determines the outcome.
What a Computer Does Instead
A computer cannot observe any of these directly, so a hardware generator samples them. The raw bytes come from somewhere like /dev/urandom or the equivalent operating system source, which is itself seeded from physical processes. That is a true random number generator.
The far more common case is a pseudo-random number generator: a deterministic algorithm producing a sequence that merely looks random.
next = (previous × multiplier + increment) mod modulus
The critical property is not subtle. If you know the algorithm and the seed, you can compute every value the sequence will ever produce. Not approximately — exactly.
This is why Math.random(), which almost every website uses, is unsuitable for anything that matters. Its output is determined by internal state. Recover that state from observed outputs and every future roll becomes predictable.
The most commonly cited example is that a linear congruential generator’s full state can be recovered from a handful of consecutive outputs. Mersenne Twister, the default in Python, needs 624 outputs to reconstruct — and 624 is a number you will reach in seconds.
Xorshift and similar fast generators have the same property in a different form: excellent statistical quality, entirely predictable if you have the state.
The Middle Ground That Actually Matters
This is the part most explanations skip, and it is the part that decides the answer in practice.

Modern web platforms do not use raw /dev/urandom per call. They use a cryptographically secure PRNG — a deterministic algorithm, but seeded from hardware entropy and designed so that observing outputs does not let you recover the state.
The distinction is worth being precise about, because the two get conflated:
| Hardware TRNG | CSPRNG | Ordinary PRNG | |
|---|---|---|---|
| Based on physical entropy | Yes | Yes | No |
| Deterministic given the state | No | Yes | Yes |
| Output reveals the state | No | No | Yes |
| Suitable for a prize draw | Yes | Yes | No |
A CSPRNG is deterministic in the same sense that every piece of software you use is deterministic. What makes it acceptable is that the state is not recoverable from what it has already emitted.
Browsers expose exactly this through crypto.getRandomValues(). It is the source RandomSelect draws from.
Fairness Is a Second Question
Uniformity is easy to get wrong even with a good source. A d6 is straightforward: six equally likely values. A d7 is not, and neither is “pick 1 of 40 names.”
The naive approach — scale a random float from 0 to 1 onto the range — biases the low end. With 40 names, the first would come up slightly more often than the last, because some random values land past the end of the last bucket and get discarded unevenly.
The correct approach is rejection sampling: draw, and if the result falls in the incomplete final bucket, discard it and draw again. The distribution becomes exactly uniform.
This is easy to overlook because the bias is small. In practice, the implementation rejects out-of-range values and redraws, so every entry has exactly a 1/N chance — including in a 40-name draw, where a scaled-float implementation would give the first name a very slightly higher probability.
The same technique appears in the short-ID generator, where the comment in the code notes the acceptance rate and the resulting bias of about two parts in ten million. Small enough to be irrelevant, and small enough that a careless implementation would have gone unnoticed — which is exactly the point of measuring it.
What This Means in Practice
For picking a lunch, none of this matters. Any generator produces an acceptable lunch.
For a prize draw, an employee assignment, or anything with a reputation attached, the question is whether the output could have been anticipated. With Math.random() it could, in principle. With crypto.getRandomValues() it cannot — which is a difference between “we say it was random” and “the mechanism makes it so”.
There is a related and frequently confused point about the animation. On a spinning wheel, the result is fixed before the wheel starts moving; the animation plays toward it. Some readers take this as evidence of manipulation. It is the opposite: because the outcome is already determined and cannot be influenced by timing, there is no input for anyone to exploit. The visual is theatre over a value that is already fixed.
Testing It Yourself
A quick exercise that needs no code: roll a die twenty times and write down the results. Then look for streaks.
You will find them. Three sixes in a row happens roughly once in every fifty rolls. This is not a sign the die is loaded — it is what a fair process looks like when you examine a short sample.
Then roll a hundred times and count each face. The gaps narrow. Nobody is converging, because there was never a deficit to close: this is what independence means when you look closely enough.
Where to Read More
The fairness page on this site walks through the actual implementation, including the rejection-sampling step. It is worth reading once, particularly if you are choosing a tool for a decision that will be questioned.
The practical summary is short: a tool that uses crypto.getRandomValues() and rejects out-of-range draws is doing the right thing, and you do not need to take its word for it — you can reason about the mechanism instead.