Randomness
Choosing between Math.random() and crypto.getRandomValues()
What each JavaScript random API actually returns, why one is predictable, and how to write an unbiased integer picker with rejection sampling.
8 min readUpdated
JavaScript gives you two very different ways to get a random-looking number, and picking the wrong one has real consequences. One is fast and fine for things nobody will ever contest. The other is slower, more code, and the only reasonable choice the moment money, prizes, or fairness claims are involved.
What each API actually returns
Math.random()
function Math.random(): number // [0, 1)
const x = Math.random(); // e.g. 0.7321...
const roll = Math.floor(x * 6) + 1; // naive 1-6 mapping, see belowcrypto.getRandomValues()
function crypto.getRandomValues<T extends ArrayBufferView>(array: T): T
const buf = new Uint32Array(1);
crypto.getRandomValues(buf);
const x = buf[0]; // a random 32-bit unsigned integer, 0 to 4294967295That difference in shape isn't cosmetic — filling an array instead of returning a scalar is precisely what makes batching efficient, which matters once you're drawing thousands of values (more on that below).
When Math.random() is the right tool
Math.random()
Why it's the wrong tool for anything contested
The problem is predictability of internal state. Xorshift128+ and similar engine-internal PRNGs are fast precisely because they're simple linear operations over a small state (128 bits, in that case). Researchers have published working methods to observe a handful of consecutive Math.random()Math.random()
What a cryptographically secure generator promises — and what it doesn't
crypto.getRandomValues()
It is not a guarantee about auditability or tamper resistance against whoever runs the page. A CSPRNG being unpredictable to an outside observer says nothing about whether the person operating the tool secretly swapped the list of names beforehand, re-ran the draw silently until they liked the result, or hard-coded an outcome. "Uses a secure random source" and "produces a result nobody, including the operator, could have influenced" are two different claims, and no client-side JavaScript API can prove the second one on its own — that requires a transparent process (visible participant list, a single visible draw, no re-rolling) around the tool, not just the tool itself. For more on how randomness works on RandoKit specifically, see the dedicated page.
Performance and batching
Calling crypto.getRandomValues()Math.random()
function makeRandomPool(size = 1024) {
const pool = new Uint32Array(size);
let index = size; // force a refill on first use
return function nextUint32() {
if (index >= pool.length) {
crypto.getRandomValues(pool);
index = 0;
}
return pool[index++];
};
}
const nextUint32 = makeRandomPool();
// nextUint32() is now cheap to call thousands of timesThis is the kind of detail that matters when generating, say, a hundred thousand rows for the random number generator tool, or shuffling a large roster repeatedly with the random winner picker, but is irrelevant if you're drawing one winner from twenty names.
An unbiased integer helper with rejection sampling
Neither Math.floor(Math.random() * n)randomByte % nn
function secureRandomInt(maxExclusive: number): number {
if (maxExclusive <= 0) throw new Error("maxExclusive must be positive");
// Largest multiple of maxExclusive that fits in a 32-bit range
const range = 0x100000000; // 2^32
const limit = range - (range % maxExclusive);
const buf = new Uint32Array(1);
let value: number;
do {
crypto.getRandomValues(buf);
value = buf[0];
} while (value >= limit); // reject and redraw on the biased leftover
return value % maxExclusive;
}
// secureRandomInt(6) is uniform over 0-5 with no modulo biasThe rejection loop runs again only on the rare draws that fall in the unevenly-divided leftover range, so in practice it almost always finishes on the first try, and the cost of an occasional extra draw is trivial compared to the correctness it buys.
Practical guidance: which one, when
- Use Math.random(): UI animation timing, decorative shuffles, placeholder content, anything where a technically savvy user predicting the outcome causes no harm.
- Use crypto.getRandomValues() with rejection sampling: picking a name on the wheel of names, drawing a winner with the random winner picker, generating numbers for a lottery or bracket seed, or any situation where someone could reasonably ask "how do I know this wasn't rigged or predictable?"
The takeaway
The two APIs aren't interchangeable conveniences — they trade off speed and simplicity against predictability guarantees, and the right choice depends entirely on whether a human stands to gain or lose something from the outcome. For a deeper look at what's happening underneath both approaches, including seeds, entropy, and why modulo bias shows up at all, read how random number generators work.