Random Number Generator Guide: Online Random Number Applications
Generating random numbers is easy. Getting them right is not. The same instruction — "pick a number between 1 and 100" — can be perfectly fair in one implementation and twice as likely to return some values as others in another. A shuffle routine can quietly give certain orderings a 25% higher chance than the rest. The generator is rarely the culprit; the random source, the modulo step and the sampling algorithm usually are.
This guide skips the button-by-button walkthrough and focuses on four issues that actually cause trouble: where pseudo-random ends and cryptographically secure random begins, why a seed is both a reproducibility feature and a fatal weakness, how modulo arithmetic destroys uniformity and how to repair it, and how to test randomness without fooling yourself. Every figure below comes from exact enumeration or a repeatable simulation.
1. Between pseudo-random and true random lies an engineering boundary
Dividing random numbers into "true" and "pseudo" is not precise enough for engineering work. The boundary that matters runs inside pseudo-randomness: on one side sits an ordinary PRNG, on the other a cryptographically secure PRNG (CSPRNG). Both can pass basic uniformity checks. The difference is not how random the output looks, but whether it can be predicted.
A PRNG is designed for speed and statistical uniformity, and its internal state is fully determined once an observer recovers it. JavaScript's Math.random() is the classic example: mainstream engines use a fast non-cryptographic algorithm, and the specification only promises a number in the interval from 0 up to (but not including) 1. It never promises unpredictability. Using it for password-reset tokens, raffle winners or coupon codes hands your security boundary to a predictable sequence.
A CSPRNG targets a different property: even with every previous output known, the next one cannot be derived. In the browser that means crypto.getRandomValues(); on the server it means the operating system's random device or a cryptographic library. It is slower, but a single call still takes microseconds, which is more than fast enough for tokens, keys, verification codes and session identifiers.
The test is simple: would unpredictability affect money, accounts or fairness? If yes, use a CSPRNG. If no — simulating dice, generating test fixtures, picking a student at random — an ordinary PRNG is entirely appropriate, and there is no reason to pay the cryptographic cost on every call.
2. Seeds: reproducibility and unpredictability cannot both win
A pseudo-random sequence is determined by its seed. Identical seed, identical sequence. That property matters enormously on both sides of the boundary, in opposite directions.
In testing and simulation a fixed seed is essential. A defect that only appears under one particular batch of random data can never be reproduced unless the seed is pinned. Write the seed into the test case, the log or the configuration, and any run can be replayed exactly — which is what makes randomized testing usable as regression testing.
In security, reproducibility is a disaster. If the seed comes from the current timestamp, an attacker who knows roughly when a service started can shrink the search space to a small window and try every candidate; a space of astronomical size collapses into a few minutes. Most real-world "predictable token" incidents were not caused by broken algorithms but by seeds with visible structure. A security seed must come from an unpredictable source — never a timestamp, a process ID or an incrementing counter.
The practical answer is to keep two separate paths: fixed-seed ordinary PRNGs for tests and experiments, where results must be recomputable, and CSPRNGs for anything production-facing that touches accounts, money or fairness, with the seed supplied by the system and never handled by business code.
3. Modulo bias: why rand() % n is not uniform
This is the easiest trap to miss and one of the cheapest to fix. Suppose your random source produces integers uniformly from 0 to 255 — one byte — and you need a value from 0 to 99, so you take the remainder after dividing by 100. The problem appears immediately: 256 is not a multiple of 100.
Group those 256 values by remainder. Remainders 0 through 55 each have three sources (0, 100 and 200 all yield 0), while remainders 56 through 99 have only two. The first 56 values therefore appear with probability 3/256, the remaining 44 with probability 2/256 — a ratio of 1.5 to 1. That is modulo bias.
How large the bias gets depends on the arithmetic relationship between the source range and the number of buckets. The table below was produced by exact enumeration:
| Random source range | Modulo target | Highest probability | Lowest probability | Ratio (high ÷ low) |
|---|---|---|---|---|
| 0 – 9 (10 values) | 6 | 20.0000000% | 10.0000000% | 2.0000000000 |
| 0 – 255 (8-bit) | 100 | 1.1718750% | 0.7812500% | 1.5000000000 |
| 0 – 255 (8-bit) | 6 | 16.7968750% | 16.4062500% | 1.0238095238 |
| 0 – 32767 (15-bit) | 1000 | 0.1007080078% | 0.0976562500% | 1.0312500000 |
| 0 – 32767 (15-bit) | 100 | 1.0009765625% | 0.9979248047% | 1.0030581040 |
| 0 – 4294967295 (32-bit) | 100 | 1.0000000009% | 0.9999999776% | 1.0000000233 |
Two conclusions here are counter-intuitive. First, bias is a matter of degree, not of presence. With a 32-bit source the gap between the most and least likely value is only 0.000000023 percentage points, which no application will ever notice; with a 10-value source and 6 buckets, the best case is twice as likely as the worst. Second, what decides the outcome is the width of the underlying range, not whether the code contains a modulo operation. The same % 100 behaves completely differently behind an 8-bit source and behind a 32-bit one.
There are three fixes, ordered by cost:
- Widen the range to a multiple. Draw from 0 to 999 first, then take the remainder modulo 100. Because 1000 is a multiple of 100, every bucket has exactly the same number of sources and the bias is zero — provided a wide enough source is available.
- Rejection sampling. Accept only values inside the largest complete block and redraw otherwise. For 0 to 255 modulo 100, accept 0 through 199: the acceptance rate is 78.125% and the expected number of draws is 1.28. For 0 to 32767 modulo 1000 the acceptance rate is 97.65625% and the expected count is 1.024. The cost is negligible and the result is exact.
- Use an unbiased API. Most languages and libraries already offer "uniform integer in a range" functions that perform rejection sampling internally. If one exists, do not hand-roll the modulo.
One useful side note: when the number of buckets divides the source range exactly — drawing multiples of 10 from 0 to 999, for instance — the modulo is inherently unbiased. Picking a number between 1 and 100 is usually safe for the same reason: modern random sources are far wider than 100.
4. Four worked examples
Example 1: bias is invisible in small samples. The same biased method — an 8-bit source modulo 100 — simulated at three sample sizes:
| Sample size | Most frequent value | Least frequent value | High ÷ low |
|---|---|---|---|
| 1,000 draws | 20 (2.0000%) | 0 (0.0000%) | not comparable |
| 10,000 draws | 148 (1.4800%) | 56 (0.5600%) | 2.643 |
| 1,000,000 draws | 11,946 (1.1946%) | 7,672 (0.7672%) | 1.557 |
| Theoretical value | 1.1719% | 0.7813% | 1.500 |
At 1,000 draws one value never appeared at all, which looks like a serious implementation bug but is simply noise masking the bias. Only at one million draws does the observed ratio converge towards the theoretical 1.5. A small sample that looks even proves nothing about the algorithm; judging randomness by "it looked fine when I tried it" points in the wrong direction.
Example 2: a broken shuffle gives some orderings 25% extra probability. Shuffle three elements by picking an index from the full range on every pass — instead of from the current position onwards — and you get 3 × 3 × 3 = 27 equally likely paths. They do not land on the six permutations equally often:
| Permutation | Count (of 27 paths) | Probability | Uniform would be |
|---|---|---|---|
| 0-1-2 | 4 | 14.8148% | 16.6667% |
| 2-0-1 | 4 | 14.8148% | 16.6667% |
| 2-1-0 | 4 | 14.8148% | 16.6667% |
| 0-2-1 | 5 | 18.5185% | 16.6667% |
| 1-0-2 | 5 | 18.5185% | 16.6667% |
| 1-2-0 | 5 | 18.5185% | 16.6667% |
The high-to-low ratio is 1.25. The more elements involved, the harder the distortion is to spot by eye: three elements can be enumerated by hand, but thirty elements have thirty-factorial orderings and the bias hides completely behind "it looks shuffled". The correct method is the Fisher-Yates shuffle: on pass i, swap only with an element at index i or later. For three elements it produces exactly 3 × 2 = 6 paths, one per permutation, at 1/6 each. To draw k distinct items, shuffle only the first k passes — the cost is O(k), with no need to sort the entire list and slice it.
Example 3: repeats are a mathematical certainty, not a malfunction. Drawing with replacement from 100 numbers, the chance of at least one repeat is 37.18% after 10 draws and 49.68% after 12 — close to a coin flip. The birthday problem behaves the same way: with 23 people, the probability that two share a birthday is 50.73%. A repeated winner in a raffle is therefore no evidence that the tool is broken. Uniqueness has to be guaranteed by the algorithm — a no-repeat mode, or shuffling before drawing — never by drawing "only a few" times.
Example 4: use a chi-square test to judge evenness. Roll a six-sided die 6,000 times; each face should appear 1,000 times. If the observed counts are 1013, 980, 1024, 961, 998 and 1024, the chi-square statistic is 3.2460. With 5 degrees of freedom and a significance level of 0.05, the critical value is 11.070; because the statistic is below it, the hypothesis of uniformity cannot be rejected. Feed in a visibly skewed set instead — 1200, 1150, 900, 850, 950, 950 — and the statistic reaches 100.0000, far above the threshold, so the bias is judged real.
5. How much randomness each use case needs
Mapping those results onto real workloads produces a clear hierarchy:
| Use case | Randomness required | Recommended approach | Common mistake |
|---|---|---|---|
| Passwords, keys, session tokens, verification codes | Cryptographically secure, unpredictable | CSPRNG API; favour length over character-set complexity | Building them from an ordinary PRNG or a timestamp seed |
| Raffles, lotteries, random grouping | Statistically uniform and auditable | Fix the range and count in advance; keep the seed and the output | Modulo on a narrow source; deciding the rules after the draw |
| Sampling k distinct items | Statistically uniform | Fisher-Yates, first k passes only | Sorting with a random comparator, or drawing from the full range each pass |
| Test fixtures, simulation, randomized tests | Reproducibility first | Pin the seed and log it | Not recording the seed, so failures cannot be replayed |
| A/B assignment | Stable and reproducible, balanced groups | Hash a user identifier into a bucket, or assign in interleaved order | Re-randomizing per request, so users keep switching arms |
| Publishing or auditing a result | Verifiable | Publish the rules first; record the seed and the output | Publishing the outcome without the process |
The A/B row deserves attention: it needs reproducibility rather than unpredictability. Re-randomizing on every request makes the same user bounce between arms and invalidates the experiment. Hashing a stable user identifier into a bucket guarantees that a user stays in one group.
6. Randomness can be tested, but passing a test proves nothing
The quickest way to sanity-check a source is a histogram: bucket the outputs and compare the counts. A uniform source produces bars of similar height. The eye only catches gross problems, so quantitative judgement comes from a chi-square test — degrees of freedom equal the bucket count minus one, and a statistic below the critical value means the deviation is still within random fluctuation.
Keep the limits of testing in mind: a test can falsify, never prove. A deterministic sequence can sail through a uniformity check. Output the numbers 0 to 99 one million times in a loop and every value appears exactly ten thousand times — a perfectly flat histogram, a chi-square statistic of 0, and no randomness whatsoever. Conversely, a genuinely random source often looks uneven in a small sample, exactly like the gaps in Example 1.
So the order of judgement should be: decide which strength the use case needs, verify that the implementation uses the right API, and only then run statistical checks as a health check. Doing it backwards — reading a histogram to decide whether something is "random" — leads to the wrong conclusion.
7. Frequently Asked Questions
- Why can't Math.random() generate verification codes or tokens?
- It is a non-cryptographic pseudo-random generator: the specification promises only a value range, not unpredictability, and once its internal state is recovered the following outputs are fully determined. Tokens, keys and verification codes should come from a cryptographically secure interface such as crypto.getRandomValues().
- Does taking a modulo bias a lottery between 1 and 100?
- It depends on the width of the underlying source. With a 32-bit or wider uniform integer, the ratio between the most and least likely outcome is 1.0000000233, which is irrelevant in practice. The dangerous case is a narrow source: with 10 possible values and 6 buckets, the most likely outcome is twice as likely as the least likely one.
- Why do random numbers repeat — is the tool broken?
- Repeats are a consequence of probability, not a defect. Drawing with replacement from 100 numbers, the chance of at least one repeat after 12 draws is about 49.68%, close to one in two. Uniqueness must be guaranteed by the algorithm, for example with a no-repeat mode or by shuffling before drawing.
- Is a fixed seed safe?
- It depends on the purpose. Tests, simulations and randomized test cases need a fixed seed so results can be reproduced, which is a benefit. In security, an enumerable seed is a fatal weakness: seeding from a timestamp reduces the search space to a few minutes, so an unpredictable source is mandatory.
- How can I tell whether a sequence of random numbers is uniform?
- Start with a histogram to see the shape of the distribution, then use a chi-square test for a quantitative answer: for 6,000 rolls of a six-sided die there are 5 degrees of freedom and a critical value near 11.070, so a statistic below it cannot be declared abnormal. Remember that passing only means "not falsified" — a deterministic loop that outputs every value a million times passes perfectly.
8. Three common misconceptions
Misconception 1: if it looks messy, the randomness is good. Human intuition is poor at this. Genuinely random sequences contain long runs and visible clusters, while a deliberately "spread out" sequence looks smoother than random. Judge with statistics and code review, not by eye.
Misconception 2: a little post-processing turns Math.random() into secure randomness. Multiplying the result by a large number, keeping a few decimal places or appending a timestamp adds no unpredictability at all — every one of those steps is reversible. Security strength comes from the entropy of the source, not from decoration applied afterwards.
Misconception 3: a lottery is fair as long as it does not repeat. Uniqueness only prevents the same person winning twice; it says nothing about whether every ticket had an equal chance. With a biased modulo underneath, or with entries taken from a pre-ordered list, a "no repeats" list can still be systematically tilted.
9. Four practical recommendations
- Classify the randomness before choosing an API. Anything touching accounts, money or fairness uses a CSPRNG; simulations, tests and entertainment use an ordinary PRNG with a pinned seed. Once the classes are defined, code review has a clear standard.
- Do not hand-roll the modulo. Prefer a library function that returns a uniform integer in a range. If you must write it yourself, add rejection sampling and check whether the source range divides the bucket count exactly.
- Archive the seed together with the result. Raffles, sampling and random grouping often need to be explained afterwards. Record the range, the count, the seed and the final output so anyone can replay the draw — far more convincing than the phrase "the system picked randomly".
- Use statistical tests as a health check, not as proof. Run a histogram and a chi-square check periodically on critical random workflows to catch implementation-level bugs such as a wrong range or a degenerate modulo. Do not expect them to certify that a source is truly random.
If all you need is a list of random integers inside a fixed range, the random number generator covers it — enable the unique mode to avoid the repeats described in Example 3. For passwords and tokens, switch to the password generator so that the strength of the random source matches the purpose. To go deeper on why unpredictability matters, read the password generator guide and the UUID generator guide; for how random identifiers are encoded when they travel between systems, see the UUID generator and the Base64 encoder and decoder guide.