When you click "generate" on a random number tool, you expect an unpredictable result. But computers are deterministic machines — they follow instructions exactly. So how do they produce randomness?
This guide explains how random number generators work, the important differences between types, and when it matters which one you use.
What Is a Random Number Generator?
A random number generator (RNG) is any process that produces a sequence of numbers with no predictable pattern. There are two fundamental categories:
- True random number generators (TRNGs): Measure physical phenomena — radioactive decay, thermal noise, atmospheric interference. The output is genuinely unpredictable because it comes from quantum or chaotic processes.
- Pseudorandom number generators (PRNGs): Use a mathematical algorithm and a starting value (seed) to produce a sequence that looks random but is entirely deterministic. Given the same seed, the same sequence appears every time.
Most software, including web browsers and programming languages, uses PRNGs. True RNGs require specialized hardware.
How Pseudorandom Generators Work
A PRNG starts with a seed — an initial number — and applies a mathematical function repeatedly to produce a sequence.
A Simple Example: Linear Congruential Generator (LCG)
One of the oldest and simplest PRNGs is the linear congruential generator:
next = (a × current + c) mod m
Where:
ais the multipliercis the incrementmis the modulus (determines the period)- The first value is the seed
Example with a = 5, c = 3, m = 16, seed = 7:
| Step | Calculation | Result |
|---|---|---|
| 1 | (5 × 7 + 3) mod 16 | 6 |
| 2 | (5 × 6 + 3) mod 16 | 1 |
| 3 | (5 × 1 + 3) mod 16 | 8 |
| 4 | (5 × 8 + 3) mod 16 | 11 |
| 5 | (5 × 11 + 3) mod 16 | 10 |
The sequence looks random, but it is completely determined by the seed and parameters. It also repeats after at most m values (the period).
LCGs are fast but have well-known weaknesses: short periods, patterns in lower bits, and predictability. Modern PRNGs use better algorithms.
Modern PRNG: xorshift128+
Most JavaScript engines (V8 in Chrome, SpiderMonkey in Firefox) use xorshift128+ for Math.random(). It works by:
- Maintaining a 128-bit internal state (two 64-bit values)
- Applying XOR and bit-shift operations to update the state
- Returning the sum of the two state values
xorshift128+ is fast, has a long period (2¹²⁸ − 1), and passes most statistical randomness tests. But it is not cryptographically secure — an attacker who observes enough outputs can reconstruct the internal state and predict all future values.
Cryptographically Secure PRNGs (CSPRNGs)
For security-sensitive applications, predictability is unacceptable. A cryptographically secure PRNG (CSPRNG) has an additional property: even if an attacker knows every previous output, they cannot predict the next one in polynomial time.
CSPRNGs are built on:
- Block ciphers: AES in counter mode (AES-CTR-DRBG)
- Stream ciphers: ChaCha20
- Hash functions: HMAC-DRBG
These algorithms are seeded with entropy — unpredictable data gathered from the physical world.
How Browsers Provide Crypto Randomness
When JavaScript code calls crypto.getRandomValues(), the following happens:
- The browser requests random bytes from the OS
- The OS entropy pool collects data from:
- Hardware interrupt timing
- Mouse and keyboard events
- Disk I/O timing
- Network packet arrival times
- CPU instruction timing jitter
- (On some systems) dedicated hardware RNG chips (Intel RDRAND)
- The OS feeds this entropy into a CSPRNG (e.g.,
/dev/urandomon Linux, BCryptGenRandom on Windows) - The CSPRNG outputs random bytes that are computationally indistinguishable from true randomness
This is the same mechanism used for generating TLS session keys, signing tokens, and encrypting data. It is the gold standard for software randomness.
When Does the Difference Matter?
| Use case | PRNG (Math.random) | CSPRNG (crypto) |
|---|---|---|
| Animation timing | ✅ Fine | Overkill |
| Randomizing UI element positions | ✅ Fine | Overkill |
| Shuffling a playlist | ✅ Fine | ✅ Better |
| Dice rolls in casual games | ✅ Fine | ✅ Better |
| Competitive game with prizes | ❌ Predictable | ✅ Required |
| Raffle or lottery drawing | ❌ Predictable | ✅ Required |
| Password/token generation | ❌ Insecure | ✅ Required |
| Scientific random sampling | ⚠️ Depends | ✅ Recommended |
| Cryptographic key generation | ❌ Dangerous | ✅ Required |
Rule of thumb: If an adversary could gain something by predicting the output, use a CSPRNG.
Mapping Random Numbers to a Range
Getting a random number is only half the problem. You also need to map it to a specific range (e.g., 1 to 6 for a die roll). This is where modulo bias can creep in.
The Modulo Bias Problem
A common approach:
// ⚠️ Biased
var result = randomByte % 6; // randomByte is 0-255
The problem: 256 (number of possible byte values) doesn't divide evenly by 6. The remainder distribution:
- Values 0–3 each map from 43 source values
- Values 4–5 each map from 42 source values
That means 0–3 are each about 2.3% more likely than 4–5. For a single die roll, this is negligible. For millions of draws or security applications, it's a real problem.
The Fix: Rejection Sampling
Rejection sampling discards values that would cause bias:
function secureRandom(min, max) {
var range = max - min + 1;
var bitsNeeded = Math.ceil(Math.log2(range));
var mask = (1 << bitsNeeded) - 1;
while (true) {
var buf = new Uint8Array(Math.ceil(bitsNeeded / 8));
crypto.getRandomValues(buf);
var value = 0;
for (var i = 0; i < buf.length; i++) {
value = (value << 8) | buf[i];
}
value = value & mask;
if (value < range) {
return min + value;
}
// Reject and retry — ensures perfect uniformity
}
}
This guarantees each output has exactly equal probability, at the cost of occasionally discarding a value. In practice, the rejection rate is under 50%, so the overhead is minimal.
Uniform vs. Other Distributions
Random number generators typically produce uniformly distributed values — every outcome has the same probability. But not all randomness in nature or statistics is uniform:
- Normal (Gaussian) distribution: Bell curve. Heights, test scores, measurement errors. Most values cluster near the mean.
- Exponential distribution: Models time between events (e.g., customer arrivals). Small values are common; large values are rare.
- Poisson distribution: Counts of rare events in a fixed interval (e.g., emails per hour).
To generate non-uniform distributions, you start with a uniform RNG and apply a transformation. For example, the Box-Muller transform converts two uniform random values into two normally distributed values.
For most everyday random number needs — picking a number, rolling dice, selecting a winner — uniform distribution is exactly what you want.
The Gambler's Fallacy
One of the most persistent misconceptions about randomness:
"I've flipped heads 5 times in a row, so tails is due next."
This is wrong. Each flip is independent. The coin has no memory. The probability of heads on the next flip is still 50%, regardless of what happened before.
The same applies to random number generators. If the range is 1–6 and you've rolled 4 three times in a row, the probability of rolling 4 next time is still exactly 1/6.
The gambler's fallacy arises because humans are pattern-seeking creatures. We expect small samples to "look random" (well-spread), but randomness is clumpier than intuition predicts. Getting three 4s in a row with a fair die has a probability of 1/216 — unlikely for any specific sequence, but not evidence of bias.
Seeding and Reproducibility
Sometimes you want reproducible randomness — running the same simulation twice should give the same results for debugging and verification. This is where seeds matter.
A seed is the starting value for a PRNG. Same seed → same sequence, every time.
import random
random.seed(42)
print(random.randint(1, 100)) # Always prints 82
print(random.randint(1, 100)) # Always prints 15
CSPRNGs like crypto.getRandomValues() intentionally do not support user-defined seeds because reproducibility would defeat the purpose of unpredictability.
When to use seeded PRNGs:
- Scientific simulations (reproducible experiments)
- Game development (deterministic replays)
- Testing (consistent test data)
When to use unseeded CSPRNGs:
- Security (tokens, passwords, keys)
- Fair drawings and lotteries
- Any case where predictability is a risk
How to Generate Random Numbers in Code
JavaScript (Cryptographically Secure)
function randomInt(min, max) {
var range = max - min + 1;
var array = new Uint32Array(1);
crypto.getRandomValues(array);
return min + (array[0] % range);
// Note: for perfect uniformity, use rejection sampling
}
randomInt(1, 100); // e.g., 47
Python
import secrets
# Cryptographically secure
secrets.randbelow(100) + 1 # 1–100
# Standard PRNG (fast, seedable, not secure)
import random
random.randint(1, 100) # 1–100
PHP
// Cryptographically secure (PHP 7+)
random_int(1, 100);
// Older (not secure)
rand(1, 100);
Command Line (Linux/macOS)
# Using /dev/urandom (cryptographically secure)
shuf -i 1-100 -n 1
# Using $RANDOM (Bash built-in, 0-32767, not secure)
echo $((RANDOM % 100 + 1))
Summary
- PRNGs (like
Math.random()) are fast and good enough for non-critical uses, but predictable. - CSPRNGs (like
crypto.getRandomValues()) are the right choice when fairness or security matters. - Modulo bias makes naive range mapping slightly unfair; rejection sampling fixes it.
- Uniform distribution gives equal probability to every outcome — the right default for most random number needs.
- The gambler's fallacy is the mistaken belief that past random outcomes affect future ones. They don't.
- Seeds enable reproducible randomness for simulations and testing.
For everyday use — picking random numbers, rolling dice, drawing raffle winners — a well-implemented CSPRNG-based tool gives you fair, unpredictable results with no setup required.
Our Random Number Generator uses crypto.getRandomValues() with rejection sampling for bias-free, cryptographically secure random integers in any range.