PRNG vs CSPRNG: when each is the right random
“Random” means very different things when you’re rolling a die in a game versus generating a cryptographic key. This article walks through PRNG (pseudo-random number generator) and CSPRNG (cryptographically secure PRNG) and how to pick the right one.
True random vs pseudo-random
True random (TRNG)
Generated from physical phenomena:
- Thermal noise in CPUs.
- Quantum events like radioactive decay or photon detection.
- Environmental noise like mouse movement and disk I/O timing.
Linux’s /dev/random pulls from an entropy pool collected from such sources.
Pseudo-random (PRNG)
Generated by an algorithm that produces a “random-looking” sequence:
- Same seed → same sequence (reproducible).
- Fast, lightweight.
- Not truly random.
Used for game dice, simulations, sampling, A/B test assignment, etc.
CSPRNG: cryptographically secure PRNG
A PRNG with stronger guarantees:
- Output reveals nothing about future output.
- Even if internal state leaks, past output stays unrecoverable (forward secrecy).
Designs like ChaCha20 and AES-CTR DRBG underpin CSPRNGs.
Why Math.random() is unsafe
JavaScript’s Math.random() is a PRNG, not a CSPRNG:
- Internals vary by browser (often xorshift128+).
- Not safe for security.
- Never use it for passwords, tokens, or keys.
Math.random(); // PRNG — fine for games and simulations only CSPRNG APIs
Browser
const arr = new Uint8Array(16);
crypto.getRandomValues(arr); // CSPRNG UUIDs:
crypto.randomUUID(); // CSPRNG-backed v4 UUID Node.js
import { randomBytes, randomInt, randomUUID } from 'node:crypto';
randomBytes(32);
randomInt(0, 100);
randomUUID(); Python
import secrets
secrets.token_bytes(32)
secrets.token_urlsafe(32)
secrets.randbelow(100) The standard random module is a PRNG; secrets is the CSPRNG-equivalent.
Seeding and reproducibility
PRNGs accept a seed and produce deterministic sequences. That helps:
- Tests — reproducible runs.
- Simulations — reproducible scientific results.
- Games — share a “world seed” (Minecraft).
function mulberry32(seed) {
return () => {
let t = (seed += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const rand = mulberry32(42);
rand(); // same value each time for the same seed Math.random() cannot be seeded; bring your own PRNG when you need reproducibility.
Picking by use case
| Use case | Recommended |
|---|---|
| Game dice / visual effects | Math.random() |
| Reproducible simulation | Seedable PRNG |
| A/B test assignment | Hash of user_id (e.g. SHA-256) |
| Password generation | CSPRNG |
| Crypto keys / tokens | CSPRNG |
| UUID v4 | crypto.randomUUID() |
| Session IDs | CSPRNG (≥256 bits) |
| Lotteries / draws | CSPRNG if fairness is required |
The bias trap: % N
To get an integer in 0–99, you might be tempted to do randomInt32() % 100. That introduces bias because 2^32 isn’t divisible by 100. Some values are slightly more likely than others.
Use rejection sampling:
function unbiased100() {
const max = Math.floor(2 ** 32 / 100) * 100;
while (true) {
const r = randomInt32();
if (r < max) return r % 100;
}
} crypto.randomInt(0, 100) and equivalents handle this internally.
Summary
- PRNG — fast, seedable, not for security.
- CSPRNG — cryptographic strength, mandatory for passwords, keys, tokens.
Math.random()is a PRNG; usecrypto.getRandomValues()for security.% Nis biased; use rejection sampling or built-in helpers.
To generate random numbers within a range, the random number tool on this site supports inclusive ranges, count, and uniqueness toggles.