Home · Blog · Secure randomness

Where random numbers come from (and why Math.random isn't enough)

24 Sep 2026 · Security · 6 min read

Every password generator, every UUID, every encryption key starts with the same ingredient: random numbers. But computers are deterministic machines — the same program with the same input always does the same thing — so where does the randomness come from? And why do security guides get so agitated about Math.random() specifically?

The answers involve two completely different kinds of "random", a surprising amount of physics, and one rule that matters more than all the theory: use the right source for the job. This article explains both kinds, where the entropy actually comes from, and how to tell which one your code is using.

Two kinds of random

Pseudorandom number generators (PRNGs) are algorithms. You give them a starting value called a seed, and they produce a long sequence of numbers that looks random — evenly distributed, no obvious pattern — but is completely determined by the seed. Same seed, same sequence, every time. That's a feature, not a bug: simulations, games and procedural generation all want repeatability. JavaScript's Math.random() is a PRNG (in most engines, a variant of xorshift128+). It's fast, it's fine for shuffling a playlist or picking a random quote.

Cryptographically secure PRNGs (CSPRNGs) are PRNGs with an additional, much stronger property: even if an attacker sees a long stretch of output, they cannot predict the next number, and they cannot work backwards to the internal state. This is a high bar. It rules out Math.random() entirely — its internal state can be reconstructed from a handful of observed outputs, after which every "random" value it will ever produce is known. There are published tools that do exactly this to predict values in web apps that misused it.

The practical distinction is simple: if anyone's security depends on the unpredictability — passwords, tokens, UUIDs, encryption keys, session IDs, shuffling for gambling — you need the CSPRNG. If nobody's security depends on it, the fast PRNG is fine. The catastrophic bugs all come from using the second kind where the first was needed.

Where the seed comes from: harvesting entropy

A CSPRNG still needs a starting seed, and that seed must itself be unpredictable — you can't pull randomness from nothing. Operating systems solve this by harvesting entropy from the physical world: the exact timing of your keystrokes and mouse movements, the microscopic jitter in disk and network timing, thermal noise, and on modern hardware, dedicated random-number instructions built into the CPU that sample physical processes.

The OS mixes these unpredictable events into an entropy pool and uses it to seed the CSPRNG. On Linux this is getrandom(); on Windows, the Cryptography API; on macOS/iOS, SecRandomCopyBytes. When your browser needs secure random numbers, it asks the OS — it doesn't try to be clever itself.

Early in a machine's boot, before much has happened to harvest, entropy can be thin — this was a real historical problem for headless servers and embedded devices booting identically. Modern systems handle it with CPU hardware generators and careful seeding, but it's a good illustration of the core principle: randomness is a physical resource that has to be gathered, not a mathematical one you can compute from nothing.

SourceTypePredictable?Use for
Math.random()PRNGYes — state recoverable from outputsGames, shuffling, non-security randomness
crypto.getRandomValues()CSPRNG (OS-backed)NoPasswords, tokens, UUIDs, keys
crypto.randomUUID()CSPRNG (OS-backed)NoUUID v4 directly
OS: getrandom() et al.CSPRNGNoEverything security-sensitive, natively
CPU hardware (RDRAND etc.)Physical sourceNoSeeding the OS pool

What this looks like in the browser

In JavaScript you have exactly two random sources, and they're deliberately hard to confuse once you know them:

  • Math.random() — returns a float between 0 and 1. Fast, seedable in some engines, not secure. Fine for animations and games.
  • crypto.getRandomValues(typedArray) — fills the array with cryptographically secure random bytes from the OS. This is the secure one. Every serious browser tool — including the Password Generator and UUID Generator on this site — uses it.
  • crypto.randomUUID() — returns a v4 UUID string directly, built on the same secure source. The convenient option when a UUID is what you want.

One subtlety worth knowing: crypto.getRandomValues() gives you random bytes; turning bytes into a password or a number in a range needs care. The classic mistake is using the modulo operator (byte % poolSize), which biases the result when the pool size doesn't divide 256 evenly — some characters become slightly more likely. The correct technique is rejection sampling: discard byte values that would cause bias and draw again. It's slightly slower and exactly as random as it should be. Good generators do this; it's one of the quiet details that separates a proper tool from a quick script.

Famous failures (and what they teach)

The history of this topic is a catalogue of expensive lessons. Early versions of a major web browser generated session keys from the time of day and process ID — predictable to anyone who cared to look. An online poker site shuffled its deck with a 32-bit seed based on the clock, letting players compute the exact shuffle. A widely used Java library's "secure" random turned out to be seeded identically across devices. In each case the code looked random and passed casual inspection; it failed because the seed or the algorithm was predictable to a motivated observer.

The pattern in every failure is the same: someone used a non-cryptographic source (or a cryptographic one, seeded badly) where unpredictability mattered. The fix is equally consistent: call the OS-provided CSPRNG and don't get creative. Randomness is one of those rare areas where the boring, standard solution is also the correct one.

A checklist for your own code

If you write software — even casually — here's how to stay on the right side of all this:

  1. Default to the secure source. In the browser that's crypto.getRandomValues(); in Python it's the secrets module; in Node.js it's crypto.randomBytes(). Most languages have an equivalent explicitly documented as cryptographically secure — learn your platform's one and reach for it first.
  2. Search your codebase for the insecure one. A quick search for Math.random, rand() or random.random() near anything security-flavoured — tokens, password resets, session IDs, shuffles with stakes — is one of the highest-value audits you can run in an afternoon.
  3. Never seed a CSPRNG yourself unless you deeply understand what you're doing. The operating system already seeds it from real entropy; adding your own "extra randomness" usually adds nothing and occasionally subtracts.
  4. Don't shrink the output carelessly. Taking a 256-bit secure value and truncating it to 32 bits for a "short token" throws away most of your security margin. Size the output to the threat: short tokens still need to be unguessable in an online setting, which usually means at least 128 bits.

The honest caveat

A CSPRNG guarantees unpredictability, not much else. It doesn't make your password strong by itself — a secure random choice among weak options is still weak (rolling a fair six-sided die gives you a fair but guessable result). Length and pool size do the heavy lifting; the CSPRNG just ensures every possibility is equally likely. And none of this helps if the random value is generated securely and then stored insecurely, logged, or sent over an unencrypted channel. Secure generation is the beginning of handling secrets well, not the end.

So the rule stands: Math.random() for games and shuffles, crypto.getRandomValues() for everything an attacker might care about — and if you're ever unsure which category you're in, assume the attacker cares.

Try the UUID Generator