Pseudo-random vs true random
True randomness comes from a physical process nobody can predict in advance — atmospheric radio noise, radioactive decay, or the messy timing of things like disk access and interrupts inside a running computer. Pseudo-randomness comes from an algorithm: give it a fixed starting value, called a seed, and it produces the exact same sequence of numbers every time, forever. It only looks random because the sequence is well spread out and doesn't repeat for a very long time. Almost everything a computer calls "random," if it's produced entirely by a formula with no outside input, is pseudo-random by definition — deterministic under the hood, however it behaves on the surface.
What Math.random() actually is
Math.random() is a pseudo-random number generator, and which algorithm sits behind it is left to each browser engine rather than fixed by any specification. Most modern engines — V8, which powers Chrome, Edge and Node, among them — use a variant of xorshift128+: a small, fast generator designed for speed and good statistical spread, the kind of quality a game or a simulation needs. It was never designed to resist prediction. Its internal state is small enough that observing a short run of consecutive outputs is enough to reconstruct that state and predict everything the generator produces afterwards. That's a non-issue for shuffling a quiz or picking a random background colour. It's a real issue anywhere the output has value to someone trying to guess it.
What crypto.getRandomValues() actually is
crypto.getRandomValues(), part of the Web Crypto API, fills a typed array with numbers from a cryptographically secure pseudo-random number generator (a CSPRNG). It's seeded from the operating system's entropy pool — hardware noise sources, interrupt timing, and on most modern machines a dedicated hardware random-number instruction — then mixed through an algorithm built specifically to be infeasible to predict, even by someone who knows the algorithm and has seen previous outputs. That's what "cryptographically secure" means here: not more statistically random than Math.random(), but computationally unpredictable to an attacker. It's the same category of generator used to produce encryption keys and session tokens. It costs a little more than Math.random(), but on any modern device that cost is irrelevant for generating a handful of numbers or dice — comfortably under a millisecond either way.
Why "random" doesn't feel random
The gambler's fallacy is the belief that after several heads in a row, a fair coin is somehow "due" a tail. It isn't: a fair generator has no memory, so the next draw carries exactly the same odds as the first one did, regardless of what came before. Streaks aren't a sign of a broken generator — they're a guaranteed feature of genuine randomness. Roll a fair six-sided die six times and getting six different faces is actually less likely than getting at least one repeat, simply from the maths of drawing six times from six options. A generator that deliberately avoided repeats to "feel fairer" would, in fact, be a biased one — real fairness sometimes looks clumpy, and a run of identical results is not, by itself, evidence that anything is wrong.
Modulo bias, and how rejection sampling fixes it
Mapping a raw random value onto a smaller range is where a naive approach quietly breaks. The obvious method — take the raw value and reduce it with % range — introduces a bias whenever the range doesn't divide evenly into the number of possible raw values. Work it through for a single byte, which has 256 possible values (0–255), mapped onto a 6-sided die. 256 ÷ 6 leaves a remainder of 4: the first 252 raw values split perfectly evenly, 42 apiece, across the 6 outcomes, but the final 4 raw values (252–255) have nowhere even to go and end up giving the lowest few outcomes one extra chance each. Taken directly, that byte-to-die mapping is measurably unfair.
Rejection sampling fixes it by refusing to use the leftover band at all: any raw draw that falls in it is discarded and a fresh one is drawn instead. What's left maps perfectly evenly onto the target range, so every outcome really is equally likely, at the cost of an occasional extra draw — roughly 4 wasted draws in every 256 here, or about one in sixty-four. In practice the tools on this site draw from a much wider integer than a single byte, so the leftover band shrinks to a handful of values out of several billion — the same mechanism, with the wasted-draw rate pushed down from roughly one in sixty-four to something close to negligible.
What fairness means for a dice roll
Fairness has two separate layers, and it's worth keeping them apart. The first is that each face of a single die comes up equally often — exactly what rejection sampling guarantees. The second is that separate dice are drawn independently of each other, so nothing links the value one die shows to the value another shows. Roll 2d6 — two dice with 6 sides each — and the totals aren't flat across the whole range 2 to 12. Seven comes up more often than two or twelve, but that has nothing to do with the generator: it's a counting fact about the notation. There are six different ways for two independent dice to sum to seven (1+6, 2+5, 3+4, 4+3, 5+2, 6+1) and only one way each to sum to two or twelve. A generator can be perfectly fair and still produce a lumpy-looking distribution for a sum — that lump is the maths of addition, not a flaw in the randomness underneath it.
When browser randomness is fine, and when it isn't
For games, shuffling a list, picking a name out of a hat, or sampling a subset of records to spot-check, Math.random()'s speed is more than enough and its predictability doesn't matter, because nobody stands to gain anything from guessing the next value. It becomes a genuine problem the moment the output has value to someone trying to predict or reproduce it — a password or passphrase, a security token, a raffle with a prize worth gaming, anything where an observer who could reconstruct the sequence would benefit from doing so. In those cases only a cryptographic source belongs in the mix: crypto.getRandomValues() in a browser, or an equivalent operating-system-level generator on a server. The random number generator, dice roller and password generator on this site all draw from crypto.getRandomValues() with rejection sampling for exactly this reason — the cost is negligible and it removes any question about whether the source can be trusted.
Frequently asked
Is Math.random() insecure?
Insecure for anything where prediction has a payoff — yes. Fine for a game, a shuffle, or picking a random example — also yes. The two aren't in conflict; they're answers to different questions about what the number is being used for.
Why does rejection sampling waste any draws at all — why not build a generator that outputs the range directly?
The underlying hardware and operating-system entropy sources naturally produce uniform bits and bytes, not uniform values over an arbitrary range like 1–6. Rejection sampling is the standard technique for turning "uniform over a power of two" into "uniform over any range" without bias, and because the wasted band is tiny relative to the full range of a 32-bit integer, the practical cost is negligible.
Can crypto.getRandomValues() ever produce the same sequence twice?
Only by astronomical coincidence. Unlike a seeded generator you could reset to a known starting value, a CSPRNG draws fresh entropy from the operating system on every call, so there's no seed for a page or a script to control or repeat.
Does more randomness make dice rolls "fairer" over many rolls?
Fairness is a property of the process — equal probability on every single roll — not a promise about any particular run of results. A fair die can still run hot or cold over any finite stretch of rolls; that's not evidence of bias, it's exactly what independent, equally likely outcomes look like in a limited sample. Try it yourself with the dice roller or the random number generator — for example 5 numbers between 1 and 100 — and the streaks and gaps you see across repeated presses are the fairness, not a departure from it.