PRNG vs CSPRNG: when each is the right random

3 min read

“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 caseRecommended
Game dice / visual effectsMath.random()
Reproducible simulationSeedable PRNG
A/B test assignmentHash of user_id (e.g. SHA-256)
Password generationCSPRNG
Crypto keys / tokensCSPRNG
UUID v4crypto.randomUUID()
Session IDsCSPRNG (≥256 bits)
Lotteries / drawsCSPRNG 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; use crypto.getRandomValues() for security.
  • % N is 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.