How our random number generator works
No black box. Here is the whole pipeline, the maths behind the fairness, and the real code, in plain language first and technical detail second.
The pipeline
From hardware noise to your number, in four steps
-
1. Raw randomness
We read 32-bit words from crypto.getRandomValues, your operating system’s secure generator, seeded with hardware noise.
-
2. Remove bias
We turn words into a number in your range using rejection sampling, so every outcome has exactly the same chance.
-
3. Build the tool
Dice, coins, lists, teams, lottery tickets and UUIDs are all built from that one trusted unbiased source.
-
4. Test it
Automated statistical tests run on every change, and you can run a 10,000-roll test yourself on the home page.
The fairness trick
Why “random % 6” is not fair, and what we do instead
A computer gives us random numbers from 0 to 4,294,967,295 (that is 232 values). To
roll a six-sided die you might be tempted to write random % 6. But 4,294,967,296
is not a multiple of 6: it is 715,827,882 × 6 with 4 left over. Those 4 spare values would make the first four faces very
slightly more likely.
So we throw those spare values away and draw again. It happens about once in a billion rolls, costs nothing, and makes every face exactly equally likely. The effect is dramatic for small generators: squeezing a random byte (0 to 255) into 1 to 100 makes the numbers 1 to 56 about 50% more common, which you can see for yourself in the Fairness Lab.
The actual code
export function randomBelow(src: RandomSource, n: number): number {
if (n <= 2 ** 32) {
// Largest multiple of n that fits in 32 bits.
const limit = 2 ** 32 - (2 ** 32 % n);
let x = src.u32();
while (x >= limit) x = src.u32(); // retry: these would bias it
return x % n;
}
/* wider ranges: draw just enough bits, retry if outside [0, n) */
} Simplified from src/lib/rng/random.ts. Ranges wider than 32 bits, and BigInt
ranges up to 1,000 digits, use the same idea.
Tool by tool
Which method powers what
| Tool | How it picks |
|---|---|
| Random number generator | Rejection sampling over big-integer ranges (up to 1,000 digits). No repeats: Floyd’s algorithm, then a shuffle. |
| Dice roller | One unbiased integer in [1, sides] per die. Advantage rolls two and keeps the higher or lower. |
| Coin flip | One fresh random bit per flip, read from 32-bit words. |
| Name picker | Unbiased pick (or Floyd sampling for several); teams come from a Fisher-Yates shuffle dealt round-robin. |
| Lottery numbers | Floyd sampling for each group of balls, then sorted. Odds are exact combinations, not estimates. |
| Random strings | One unbiased pick from your character set per position. Entropy = length × log2(set size). |
| UUIDs | RFC 9562: v4 has 122 random bits; v7 has a 48-bit millisecond timestamp plus random bits. |
| Seeded mode | Text → 128-bit hash (cyrb128) → sfc32 generator. Same seed, same numbers, on every device. |
Honest limits
What this is, and is not
- It is a cryptographically secure pseudo-random generator, not a physical (true random) source. It is constantly re-seeded with unpredictable hardware noise, which is why it is trusted for encryption keys. If you need certified physical randomness, use a hardware or atmospheric-noise service.
- Seeded mode is predictable on purpose. Anyone with the seed can reproduce your numbers. Use Secure mode for anything that must stay unpredictable.
- The animations are decoration. The spinning digits, tumbling dice, tossed
coin and spinning wheel all land on a result that was chosen first. Even the animation’s
randomness comes from the same secure generator, and our code is not allowed to use
Math.random(). - Lottery tools cannot improve your odds. Every combination is equally likely.
Sources
Run the test yourself
Ten thousand rolls, one chart, zero trust required.