Ask whether Math.random() is "really random" and you will get a confident answer within seconds, usually that it is not, usually followed by advice to use crypto.getRandomValues() instead. The advice is often right. The reasoning behind it is almost always wrong, and the wrongness matters, because it teaches you to check the one property that will not tell you anything.

We set out to demonstrate this with an experiment on our own tools. The experiment worked. Our first explanation of it did not survive being implemented a second way, and correcting it produced a better guide than the one we set out to write — so the correction stays in, near the front, where it belongs.

The experiment, and the mistake we made with it

Every tool on this site that draws a random integer uses the same helper. It asks the source for a 32-bit number, throws the number away if it falls in a range that would skew the result, and otherwise reduces it with % n. The technique is called rejection sampling and it exists to remove modulo bias:

function randInt(next, n) {
  // 2^32 - (2^32 mod n)
  const limit = 4294967296 - (4294967296 % n);
  let x;
  do { x = next(); } while (x >= limit);
  return x % n;
}

The idea was to leave that code completely alone and change only next — the source of the raw bits. First a cryptographic source, then a linear congruential generator, the classic textbook pseudorandom recipe: multiply the previous number by a large constant, add another, keep the low 32 bits. We used the constants from countless C textbooks, seed × 1103515245 + 12345.

Counting 600,000 draws into n buckets and measuring the worst bucket's deviation from an even share, the difference was stark. At n = 6 the worst bucket came in 106% off. At n = 10, 107%. At n = 52 — a shuffled deck of cards — 325%. Odd values of n were nearly fine.

That pattern has a famous explanation, and we reached for it. For a generator of this kind, the lowest-order bits are the weakest: the last bit repeats with a period of at most two, the last two bits with a period of at most four, and so on. Taking a number % n for even n reads exactly those bits. Case closed, we thought: correct arithmetic wrapped around a bad source is still wrong.

Then we implemented the same generator a second way, and the explanation fell apart.

Why the naive JavaScript port of a textbook linear congruential generator collapses, and why that is not the generator's fault. Multiplying a 32-bit seed by 1103515245 produces a product of up to 62 bits, but a JavaScript double carries only a 53-bit mantissa, so the bottom nine bits are rounded away before the mask to 32 bits ever runs. The exact product 3466795386165507330 becomes 3466795386165507072 as a double. Measured over 200,000 outputs, the low four bits of the naive port land on zero 196,602 times, or 98.3 percent, while the same generator written with Math.imul produces exactly 12,500 of each of the sixteen values.
The collapse was a JavaScript defect, not a generator one

Written the obvious way in JavaScript, s = (s * 1103515245 + 12345) >>> 0, that multiplication produces a number up to 62 bits wide. A JavaScript number is a double, and a double carries 53 bits of mantissa. The bottom nine bits of the product are rounded away before the mask to 32 bits ever runs.

So the low bits of our "textbook generator" were not weakly randomised. They were floating-point rounding debris. Over 200,000 outputs, the bottom four bits landed on zero 196,602 times — 98.3% — and twelve of the sixteen possible values never appeared at all. Then we took that number % 6.

The collapse was real and reproducible, but it came from a bug in our test harness, not from the generator property we were blaming. It is a JavaScript footgun, and anyone porting integer arithmetic from C without Math.imul will hit it. It just is not evidence of anything about linear congruential generators.

Implemented properly, the textbook generator is uniform

Math.imul performs exact 32-bit multiplication, which is what the C code assumed all along. Same helper, same threshold, same % n, same seed, 600,000 draws. Only the multiplication is now correct:

ncrypto sourceLCG, exact 32-bitLCG, naive JavaScript
30.19%0.16%4.00%
50.61%0.58%3.81%
60.48%0.31%106.13%
70.56%0.64%5.99%
100.82%0.82%107.30%
521.72%2.10%325.18%

The middle column is the one to look at. A 1960s pseudorandom generator, the textbook example of a bad one, is indistinguishable from a cryptographic source on this test. At n = 6 it beat it.

We checked the bits directly rather than trusting the buckets. Over 200,000 draws, the low four bits of the correctly implemented generator came out at exactly 12,500 each across all sixteen values. Not approximately. Exactly.

The textbook claim about short periods in the low bits is still true — we can print it:

last bit,      first 12 outputs:  0 1 0 1 0 1 0 1 0 1 0 1
last two bits, first 12 outputs:  2 3 0 1 2 3 0 1 2 3 0 1

What does not follow is the conclusion we drew from it. A generator with a full period walks through every value of those low bits before it repeats. A period of four that visits 2, 3, 0 and 1 in rotation is not biased towards anything. It is perfectly uniform and completely predictable at the same time, and the mistake we made was assuming the first of those implies the absence of the second.

What a bucket count cannot see

Randomness has two independent axes and a uniformity test can only see one. Plotted on whether a source passes a uniformity test and whether it is unpredictable to an observer: crypto.getRandomValues sits alone in the quadrant that is both, at 0.19 to 1.72 percent worst-bucket deviation. Math.random, which uses xorshift128+, and a textbook linear congruential generator implemented with exact 32-bit arithmetic both sit in the uniform-but-predictable quadrant at 0.16 to 2.10 percent, statistically indistinguishable from crypto, yet one observed output of the LCG predicted 1000 of the next 1000 draws. The same LCG ported naively into JavaScript falls into the neither quadrant at 106, 107 and 325 percent. The remaining quadrant, unpredictable but not uniform, holds nothing worth using.
Two axes, and a bucket count can only see one

Here is the same generator that just passed every uniformity test in the table, viewed the other way. For this generator the output is the internal state, so an observer who sees one number knows everything:

const secret   = lcg(0xC0FFEE);
const observed = secret();        // one draw, watched
const attacker = lcg(observed);   // ...that was the entire state

after ONE observed output: predicted 1000/1000 of the next draws

actual rolls : 1 4 3 6 1 4 1 6 3 2
predicted    : 1 4 3 6 1 4 1 6 3 2

No bucket count will ever tell you that. Counting outcomes measures whether results are evenly spread, not whether they are guessable. The two properties are independent; a source can be flawless on one and worthless on the other.

That is the whole argument, and it is why the usual framing of this topic is unhelpful. The question is not "how random is it". There is no single axis called randomness to be more or less of. There are at least two, and the test almost everyone reaches for can only see the one that usually does not matter.

In defence of Math.random

This is the point where an article of this kind normally turns on Math.random, and it should not, so we will be explicit: modern Math.random is not a generator of the sort we broke above.

V8, the engine in Chrome and Node, published the change. Its own account is that "until late 2015 (up to version 4.9.40), V8's choice of PRNG was MWC1616", an algorithm that "fails many statistical tests in the TestU01 suite". Its replacement, xorshift128+, "uses 128 bits of internal state, has a period length of 2^128 - 1" and "passes all tests from the TestU01 suite".

That is a serious generator — not an antique, not an LCG, and not one that will fail the experiment above. When we audited the sixteen tools on this site that depend on randomness, the ones built on Math.random came back clean, and we are not going to imply otherwise for the sake of a tidier argument.

V8 is also clear about the limit, in the same post: xorshift128+ "is still not cryptographically secure", and for "hashing, signature generation, and encryption/decryption, ordinary PRNGs are unsuitable" — which is exactly why it points readers at window.crypto.getRandomValues.

So the honest case for preferring crypto.getRandomValues is unpredictability, and only unpredictability. It is not more uniform, nor is it "more random" in any sense a counting test could confirm. It is unpredictable by construction, which is a promise about what an observer can infer, not about how evenly the numbers land.

Even that promise is narrower than people assume. As MDN notes, implementations "are not using a truly random number generator, but they are using a pseudo-random number generator seeded with a value with enough entropy". The difference is the seeding and the design goal, not a different kind of arithmetic.

The question that actually decides it

Uniformity does not separate the two sources, so the decision comes down to a single question: would anyone benefit from predicting this?

Not "is this important". Not "is this about money". Whether a person who could see the next value would gain something by seeing it. A prize draw among colleagues, yes — somebody wants to win. A word generator for a writing exercise, no.

The clearest illustration on this site is inside a single file. Our random picker chooses the winner with crypto.getRandomValues and animates the celebratory confetti with Math.random. Same tool, two sources, and the split is exactly the test: nobody gains anything by predicting where a confetti particle lands.

Applied across the cluster, the same test explains the whole split:

Uses crypto.getRandomValuesUses Math.random
random picker, team generator, number generator, lottery quick pick, password generator, date picker, workout pickername generators, word generator, the food pickers, and the four oracle tools

Every tool in the left column produces something a person might want to influence or foresee. Nothing in the right column does — with one family that deserves more than a table row.

The oracle tools, which are the interesting case

Four tools on this site draw 求签 lots: the Guandi and Guanyin oracle lotteries, along with the Wong Tai Sin and Yuelao oracles. Each shakes a numbered stick from a cylinder and returns the corresponding verse. All four use Math.random.

A reader arriving from the security side may read that as a shortcut. It is not. The distinction is a difference in what is being asked for, not a lower standard.

In divination the requirement on the draw is that it is unsteered — that nobody, including whoever built the tool, is placing a thumb on which verse comes up. That requirement is fully met by any decent generator. The requirement that a cryptographic source adds is unpredictability to an adversary, and a practice conducted between a person and a shrine does not have an adversary. There is no attacker to model.

Importing the security frame here does not make the tool better. It answers a question the practice was not asking. A physical 签筒 is not shaken to defeat an observer either, and nobody has ever thought it should be.

The sharpest evidence that this is about the practice rather than about standards is a divination tool on this site that draws nothing at all. Our character divination toy (測字) is deliberately deterministic — the same character always returns the same reading, and its source says so: "There is NO Math.random anywhere." That is not a shortcut either. In 測字 the character the querent offers is the input, so there is no draw to steer and nothing for a random source to do. Divination turns out not to be one requirement: some of these tools need a draw nobody steered, and one needs no draw at all.

What we found auditing our own randomness

Before writing a guide that links to our tools, we read their source code. Sixteen tools here depend on randomness and none of them had ever been reviewed, so we expected to find something. We report what we found rather than what would have made a better story.

The famously biased shuffle, sort(() => Math.random() - 0.5), appears nowhere in the codebase. Every Fisher-Yates implementation is correct, descending through the array and swapping against a fresh index each step. Every integer draw that needs rejection sampling has it. There is no confession in this section, and we say so plainly because these audits usually produce one and inventing a defect to keep the pattern would be worse than not having one.

One change came out of it, and it was waste rather than error. Two tools computed their rejection threshold from 0xFFFFFFFF rather than 2^32. A 32-bit unsigned integer holds 2^32 distinct values — 0 through 0xFFFFFFFF — so the count is one higher than the largest value. This did not introduce a bias — the threshold was still an exact multiple of n, so the remainder stayed uniform. It discarded up to n − 1 extra draws for no benefit and disagreed with the shared kernel, and it is now unified.

We also nearly filed a defect that was not one, and it is the more useful lesson. Our lottery generator's source contains zero calls to crypto.getRandomValues while its own header claims it is crypto-secure with no Math.random anywhere. That is precisely the shape of claim-versus-code gap these audits keep finding. It was not one — the cryptographic draw lives in a shared kernel file the tool calls into, and our search had been scoped to a single file. A defect hunt that greps one file will manufacture defects.

What happens when the cryptographic source is missing

Our lottery kernel throws if crypto.getRandomValues is unavailable, and the interface shows an error rather than a result. We wanted to know how often that can actually happen, and the obvious guess is wrong.

The common assumption is that this is an HTTPS problem — that a page served over plain HTTP loses the Web Crypto API. That is true of most of it, and not of this. MDN is explicit: "getRandomValues() is the only member of the Crypto interface which can be used from an insecure context." It is crypto.subtle and crypto.randomUUID that require a secure context. Random bytes do not.

Its browser support baseline is "available across browsers since July 2015", and the current global figure is around 96.9%. Every browser named as lacking it — Internet Explorer 10 and below, Opera Mini, Chrome 10 and below, Safari 6 and below, Firefox 20 and below, Android Browser 4.3 and below — now individually registers at essentially zero usage.

That error branch is close to unreachable in practice, but it is still the right design. The reason has nothing to do with how often it fires: the tool fails closed. The alternative is a prize draw that silently falls back to a predictable source at the exact moment it cannot do the thing it promises. A tool that refuses is telling the truth; a tool that quietly substitutes is not.

Where this guide stops

This guide does not cover two neighbouring subjects, because they already have their own.

If your question is about identifiers and tokens — whether a UUID is safe to use as an unguessable access token, and what changes between v4 and v7 — that is covered in UUID v4 vs v7. If your question is about entropy in the password sense, how much of it a passphrase carries and what that buys you, it is in how to build a password system that actually holds.

Four of our own guides state, in passing, that a tool uses crypto.getRandomValues "never Math.random". Every one of them asserts it without saying why. This guide is the one that owes you the reason, and the reason is not the one those sentences imply.

Randomness is not one axisEvenly spread and hard to guess are independent properties. Most writing on this treats them as the same thing.
A 1960s generator passed0.16% – 2.10% worst-bucket deviation over 600,000 draws, against a cryptographic source's 0.19% – 1.72%.
And it was fully predictableOne observed output of that same generator predicted 1000 of the next 1000 draws.
Our own first explanation was wrongThe dramatic collapse we measured was a JavaScript float-overflow bug in the harness, not a property of the generator.
Use crypto for unpredictabilityNever for uniformity. It is not more uniform, and no counting test will tell you that you need it.
getRandomValues works over HTTPIt is the only part of the Crypto interface that does. The secure-context rule applies to subtle and randomUUID.
If you are choosing a random source

Ask whether anyone benefits from predicting the result, and stop there — that single question decides it, where "is this important" does not. Do not reach for a uniformity test to settle it, because the property you care about is invisible to one, and a generator you should not be using will pass. Use crypto.getRandomValues whenever a person might want to foresee or influence an outcome, and use it for unpredictability rather than for any belief that it distributes better. Fail closed if it is unavailable rather than substituting quietly, even though it is available to roughly 97% of the web and has been since 2015. And if you port integer arithmetic into JavaScript, use Math.imul — we did not, and it cost us an explanation we had already written down.

Sources and provenance
  • All measurements are ours, taken on 2 August 2026 in plain Node with no dependencies, and are reproducible: 600,000 draws per value of n through the rejection-sampling helper used by our own tools, seed 12345, the worst bucket's percentage deviation from an even share. The bit-level figures — 196,602 of 200,000 low nibbles landing on zero for the naive port, and exactly 12,500 of each of sixteen values for the correct one — are over 200,000 outputs of each generator.
  • ⚠️ The correction is the headline and it is ours. An earlier internal write-up of this experiment attributed the 106% / 107% / 325% collapse to low-order-bit weakness in linear congruential generators. That attribution was wrong. The numbers reproduce exactly, but the cause is a 62-bit product being rounded into a 53-bit double before the mask to 32 bits. It was caught only by implementing the same generator a second time with Math.imul. We have left it in the body rather than quietly publishing the corrected version.
  • V8 blog, "There's Math.random(), and then there's Math.random()", Yang Guo, 17 December 2015, read 2 August 2026 — quoted verbatim for MWC1616, the switch to xorshift128+, the 128 bits of state and 2^128 - 1 period, the TestU01 results for both, and the statement that xorshift128+ "is still not cryptographically secure".
  • MDN, Crypto.getRandomValues(), read 2 August 2026 — quoted verbatim for the insecure-context exception and for the note that implementations use a seeded pseudo-random generator rather than a true one. Baseline status "Widely available", "available across browsers since July 2015". The 96.9% global support figure and the list of non-supporting browsers are from caniuse, whose usage data at the time of reading was dated June 2026 from StatCounter.
  • ⚠️ The claim that the low l bits of a power-of-two-modulus LCG have a period of at most 2^l is long-established reference material on these generators, not a primary source and not a finding of ours. We verified it holds for our implementation by printing the bits, and the printed sequences in the body are from that run. What we could not support, and originally asserted, is that this makes the output non-uniform.
  • ⚠️ Not attempted: we did not try to recover xorshift128+ state from Math.random output, and nothing here should be read as a claim that we did. Published work on that exists; recovering it is a substantially harder problem than reading a linear congruential generator's state, which for the generator used here is simply its own output. The guide claims only what V8 claims about its own generator.
  • The audit of our sixteen randomness-dependent tools, the crypto / Math.random split, the rejection-threshold unification and the near-miss false finding were all established by reading our own source on 1 and 2 August 2026. Detail in docs/guide17-randomness-sources.md.

General technical explanation, not security advice for a specific system. If you are building something where predicting an outcome would let somebody take money, an advantage or an identity, the choice of random source is one input among many and the rest of the design matters at least as much. Where this guide states a limit on what it established, it means it: the correction above was found by accident, and we would not have found it if the first implementation had been the only one.