When you generate a random number in JavaScript with Math.random(), you get a number that looks random. It’s different every time. It’s evenly distributed between 0 and 1. For most programming tasks — shuffling a playlist, picking a random element from an array, adding jitter to an animation — it behaves exactly like randomness should.
But it isn’t actually random. It’s pseudorandom. And the difference matters enormously when you’re generating passwords, tokens, or anything that needs to be unpredictable to an adversary.
What pseudorandom actually means
A pseudorandom number generator (PRNG) is a deterministic algorithm. It starts with a number called a seed and applies a mathematical formula repeatedly to produce a sequence of numbers that appear statistically random — they pass tests for uniform distribution, low correlation, and so on. But the sequence is entirely determined by the seed. Same seed, same sequence, every time.
This is useful for things like reproducible simulations, procedural game world generation, and unit testing — where you want random-looking data that you can reproduce exactly. It is dangerous for security, because if an attacker can determine or guess the seed, they can predict every value the generator will ever produce.
Math.random() implementations vary across JavaScript engines, but they typically use algorithms like xorshift128+ or similar fast PRNGs seeded from system time or a small amount of system entropy at startup. The seed state is finite and guessable — researchers have demonstrated the ability to predict future Math.random() outputs from observing past outputs.
Math.random() implementation used a PRNG with a state small enough to be fully reconstructable from 64 consecutive outputs. Once you know the state, you can predict all future values. This was patched, but the fundamental limitation of any PRNG remains.
What makes crypto.getRandomValues() different
crypto.getRandomValues() is part of the Web Crypto API, a browser standard designed specifically for cryptographic use. Rather than using a mathematical algorithm seeded at startup, it draws randomness from hardware entropy sources:
- CPU timing variations and thermal noise
- Disk I/O timing jitter
- Network packet timing
- Mouse movements and keyboard timing (on desktop systems)
- Hardware random number generators (RNGs) built into modern CPUs
These sources produce genuine unpredictability — their values depend on physical processes that cannot be predicted in advance. The operating system collects this entropy continuously and makes it available to applications through APIs like /dev/urandom on Linux or BCryptGenRandom on Windows. The browser’s Web Crypto API draws from the same pool.
The result is a cryptographically secure pseudorandom number generator (CSPRNG) — one where knowing all previous outputs gives an attacker no useful information about future outputs, and where the internal state cannot be reconstructed from observations.
Why this matters for passwords specifically
Consider a password generator built on Math.random(). If an attacker knows the generator was written in JavaScript running in a browser, and they can estimate when you generated your password (from a login timestamp, for example), they can narrow down the PRNG state significantly.
Depending on the implementation, an attacker might be able to reduce a 20-character password from 95^20 possible combinations down to a much smaller search space based on the predictable output of the PRNG. The password looks strong but isn’t.
A password generated from crypto.getRandomValues() has no such weakness. The output genuinely draws from the full space of possible values with no predictable pattern.
The performance trade-off that isn’t really a trade-off
Math.random() is faster than crypto.getRandomValues() because it’s a simple mathematical operation versus a system call that reads hardware entropy. For generating millions of random numbers per second in a game loop, this difference matters.
For generating a password or a token, you are calling the function at most a few hundred times. The difference in time is microseconds — completely imperceptible. There is no meaningful performance trade-off for security applications. Use the cryptographically secure version.
Where to use each
Use Math.random() for:
- Visual effects, animations, and game mechanics
- Shuffling playlists and non-sensitive lists
- Generating random data for UI testing
- Any context where an attacker predicting the output has no meaningful consequence
Use crypto.getRandomValues() for:
- Password generation
- Token and API key generation
- Session identifiers
- Cryptographic nonces and salts
- UUID v4 generation (or use
crypto.randomUUID()directly) - Any value that must be unpredictable to an adversary
crypto.getRandomValues() exclusively. The password generator, UUID generator, token generator, and number randomizer all draw from the browser’s cryptographically secure source. You can verify this by reading the source of script.js.