BrowserTools
Random Generators

Randomness Explained: Crypto.getRandomValues vs Math.random

Math.random is fast but predictable. GetRandomValues is cryptographic. Understand entropy, seeding, modulo bias, and when each belongs in your code.

By Guga — BrowserTools maintainer, Tbilisi, Georgia·Last updated 2026-09-21·6 min read·1198 words

Draws random integers from a configurable range using the browser's cryptographic random source.

generate cryptographically secure random numbers →

What randomness means for a computer

A general-purpose computer is deterministic, so every generator is ultimately a function of some initial state. A pseudo-random number generator expands a small state with a fixed algorithm, giving a long but ultimately repeating sequence. A cryptographically secure generator additionally guarantees that an observer of the output cannot recover the internal state or predict future values, which is a much stronger property than looking random. The distinction is not about distribution quality: Math.random passes the standard statistical test suites. It is about inference, whether the outputs leak the machinery that produced them. V8 makes the difference explicit. Until version 4.9.40 it used MWC1616, a 64-bit-state generator, and switched to xorshift128+, a 128-bit-state generator with a period of 2^128 − 1. The V8 team was explicit that the new generator is still not cryptographically secure and that ordinary PRNGs are unsuitable for hashing, signature generation and encryption. Getting this wrong is not theoretical: CVE-2008-0166 was a Debian patch to OpenSSL that removed seeding steps from the PRNG, leaving keys generated between 2006 and 2008 enumerable.

Why Math.random leaks, demonstrated

The output of Math.random is a 64-bit double in [0, 1), and that double exposes the top 52 bits of the generator's state. Because the state is only 128 bits, a small number of consecutive outputs constrains it enough to be solved, and published tools do exactly that with a constraint solver: given roughly five to ten consecutive floored values, recover the two 64-bit state words, then generate the rest of the sequence. The practical rule follows directly. If a value is not secret and the sequence behind it carries no meaning, Math.random is fine and faster. If a value is a password, a token, a session identifier, a reset link or a CAPTCHA, it must come from crypto.getRandomValues. A common anti-pattern deserves naming: Math.random().toString(36).slice(2, 12). It looks like a token and is not one. The result is variable length, because toString(36) drops trailing zeros, so two runs produce different-length strings from the same expression. It is drawn from a predictable sequence, and its character distribution is skewed. The fix: const bytes = new Uint8Array(16); crypto.getRandomValues(bytes); const token = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');

Modulo bias, and rejection sampling

The tempting one-liner is crypto.getRandomValues(new Uint32Array(1))[0] % range. It is subtly biased whenever the range does not divide 2^32. Work it through for range 100: 2^32 is 4,294,967,296, and dividing by 100 gives 42,949,672 with a remainder of 96. So 96 outcomes have 42,949,673 preimages and 4 have only 42,949,672. The most likely value occurs with probability 0.0100000000093 rather than 0.01, an absolute excess of about 9.3 × 10^-10. That is small enough to ignore in a simulation and wrong for anything where a draw decides money or access. Rejection sampling fixes it exactly: const range = max - min + 1; const limit = 0x100000000 - (0x100000000 % range); let x; do { x = crypto.getRandomValues(new Uint32Array(1))[0]; } while (x >= limit); return min + (x % range); Every accepted value now has the same number of preimages, and the loop discards the incomplete final block. Note the quota: one call may request at most 65,536 bytes, so a large buffer must be chunked.

Performance, measured rather than assumed

The usual claim is that getRandomValues is roughly ten times slower, and the real gap is much larger, but it mostly disappears when you batch. Measured in Node 24 on the machine this was written on, one million Math.random calls took about 5 ms, roughly 196 million per second. One million single-element getRandomValues calls took about 887 ms, roughly 1.1 million per second. The same one million values drawn through batched calls took under 1 ms, because the per-call overhead is what costs, not the generation. A single call filling a 16,384-element Uint32Array, the maximum within the quota, takes about 0.4 ms. The design rule is simple: never call getRandomValues once per character. Fill one buffer, then read from it, and refill in large chunks.

Choosing between them

Use crypto for anything an attacker benefits from predicting: passwords, session identifiers, API keys, reset tokens, nonces, salts, invitation codes, CAPTCHAs, lotteries, and anything deciding access or money. Use Math.random where unpredictability is not a property you need: animation jitter, a placeholder offset, shuffling a playlist for display, and Monte Carlo work needing billions of draws. For reproducible work, take neither. A seeded generator such as mulberry32 gives you a sequence you can replay exactly, which is what you want in a test, and it is a property a CSPRNG deliberately does not have. One caution about mixing: calling Math.random for a metric elsewhere in the page does not endanger a token drawn from a separate CSPRNG, because the two share no state. The danger is the reverse case. On the server use crypto.randomInt, and in Python the secrets module rather than random, which is a Mersenne Twister whose state can be recovered from 624 consecutive outputs.

Where this site's own generators stand

It is worth being precise here, because a blanket claim would be false. The shared utility in src/lib/utils/crypto.ts draws every integer, every pick, every shuffle and every generated UUID from crypto.getRandomValues, using rejection sampling for uniformity. The password, number, string, hex, username and UUID generators are built on it. Three generators are the exception and use Math.random deliberately: the coordinate generator, the date generator and the timestamp generator. Their own copy says why, that the values carry no secrecy and the speed difference is irrelevant at the counts those tools produce, which is the reasoning above applied honestly. A fourth use is randFloat, documented at its definition as unsuitable for passwords, tokens or security-bound draws. If you are auditing a codebase, grepping for Math.random and classifying each hit as security-relevant or not is the whole task, and it is worth doing because that classification is a judgement no tool can infer for you.

Testing randomness

Two different tests are needed, and confusing them wastes time. A uniformity test asks whether the distribution is flat, and the answer is that Math.random passes it, which is exactly why passing a chi-squared test tells you nothing about security. The test that matters is a predictability test: capture a run of outputs, feed them to a state-recovery tool, and check whether the next output matches what actually came out. Expect a failure with Math.random and a success with a CSPRNG. The second mistake is zeroising the buffer, which looks like good hygiene and mostly is not. Once a secret is in a JavaScript string it is immutable, and once a TypedArray is unreachable the garbage collector decides when to release it, not you. Overwriting a buffer before dropping it reduces the window in which the value sits in freed memory, a real but small benefit, and it is not a substitute for not keeping the secret around.

Frequently Asked Questions

Can I use Math.random for passwords?
No. V8 implements Math.random with xorshift128+, a 128-bit-state generator whose next outputs are computable from its current state. Public tooling recovers that state from a short run of observed values and predicts the rest. Use crypto.getRandomValues for any secret.
Is crypto.getRandomValues truly random?
No, and it does not need to be. It draws from the platform's cryptographically secure generator, which is seeded from the operating system and designed so that predicting one output does not help predict another. That property, unpredictability to an observer, is what you actually require.
Why do random numbers show modulo bias?
Because 2^32 is not divisible by your range. For a range of 100, 2^32 mod 100 is 96, so 96 outcomes are one draw more likely than the other 4. The gap is about 9.3 times 10^-10, negligible in practice and still worth removing with rejection sampling when fairness is the point.
Which is faster?
Math.random, by a wide margin, because it is arithmetic and getRandomValues is a call into the platform. In Node 24 on this machine, 1,000,000 Math.random calls took about 5 ms, the same number of single-element getRandomValues calls took about 887 ms, and one million values drawn in batched calls took under 1 ms. Batch and the difference largely disappears.