Where Do Random Numbers Come From in Your Browser?
Published on May 14, 2026 by The Kestrel Tools Team âą 8 min read
Itâs late, youâre shipping a feature, and somewhere in your code you write Math.random() to generate a token. Or you click a password generator in a browser tab and a 24-character string fills the field instantly. Both feel the same â a tiny bit of magic, a number out of nowhere â but only one of them is actually safe to put in front of a real user.
Browser random number generator security is one of those topics most developers absorb by osmosis: âuse crypto.getRandomValues, not Math.random.â That advice is correct, but itâs the punchline without the joke. If you donât know where the bytes come from, itâs hard to reason about edge cases â old browsers, embedded WebViews, server-side rendering, library defaults that quietly use the wrong API.
This is the same conversation that resurfaces on Hacker News every time the classic Myths about /dev/urandom essay climbs the front page. The myths are about Linux, but the misunderstandings show up one layer up â in the browser tab where you and I generate UUIDs, session IDs, password reset tokens, and the random samples we feed into client-side utilities at Kestrel Tools. Letâs walk through where those numbers actually come from.
Where do random numbers come from in your browser?
The browser gets random numbers from the operating systemâs cryptographically secure random source â /dev/urandom on Linux and macOS, BCryptGenRandom on Windows, getentropy on the BSDs â exposed to JavaScript through crypto.getRandomValues() and crypto.randomUUID(). That OS source is seeded by hardware entropy: keystrokes, mouse jitter, disk timing, network packet arrival times, and on modern CPUs the RDRAND / RDSEED instructions. The browser doesnât roll its own randomness; it borrows from the same pool that protects your SSH keys.
Math.random() is a completely separate function. It is not connected to the OS entropy pool. In every modern browser itâs an xorshift128+ (or similar non-cryptographic) PRNG seeded once per realm with a small amount of entropy and then iterated deterministically. That makes it fast and predictable in a benign sense â great for animation jitter or shuffling a deck in a game, terrible for anything an attacker would care about predicting.
Math.random vs crypto.getRandomValues: a side-by-side
Here is the practical difference, the kind you can put on a code review checklist:
| Aspect | Math.random() | crypto.getRandomValues() |
|---|---|---|
| Source of bits | Non-cryptographic PRNG, seeded from a small pool | OS CSPRNG (urandom / BCryptGenRandom / getentropy) |
| Output range | Float in [0, 1) | Fills a typed array (Uint8Array, Uint32Array, etc.) |
| Predictable from previous outputs? | Yes, with enough samples | No (computationally infeasible) |
| Safe for tokens, IDs, passwords, keys? | No | Yes |
| Available in Web Workers? | Yes | Yes |
| Blocks waiting for entropy? | No | No (modern browsers; older Safari was a pre-2019 edge case) |
| Spec | ECMAScript 2015 §20.2.2.27 | Web Crypto, W3C Recommendation |
Attacks on Math.random() are not theoretical. In 2015, security researcher Mike Malone published a working exploit that recovered the internal state of V8âs Math.random() from just a handful of outputs, then predicted every future value. Modern engines have hardened the implementation, but the contract has never changed: Math.random() makes no security promises. None.
If the value will ever be checked by an attacker â a session ID, a password reset link, a CSRF token, a password â it must come from crypto.getRandomValues().
How crypto.getRandomValues actually works
Under the hood, crypto.getRandomValues(buffer) is a thin wrapper around a system call. Hereâs the call chain on a typical Linux desktop running Chrome:
JS: crypto.getRandomValues(new Uint8Array(32))
â V8 calls into Blink
â Blink calls Boring SSL's RAND_bytes()
â BoringSSL pulls from /dev/urandom (or RDRAND if available)
â Linux kernel mixes hardware entropy â ChaCha20 stream
â 32 bytes return up the stack
A few things are worth noticing:
- The browser is not the source of randomness. Itâs a courier.
- The kernelâs CSPRNG never âruns outâ of entropy in normal operation. After it has been seeded once with enough hardware noise, it generates an effectively unlimited stream of bits using a stream cipher (ChaCha20 on Linux â„ 4.8, AES-CTR on Windows). This is the central insight from the urandom myths essay â and it applies equally to the bytes the browser hands you.
- The maximum size of a single call is 65,536 bytes. If you need more, call it in a loop. There is no performance penalty for doing so; modern implementations generate gigabytes per second.
A minimal, correct example:
// Generate a 256-bit random key as a hex string
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
Thirty-two bytes of pure CSPRNG output, suitable for an HMAC key, an OAuth state parameter, or the seed for a password.
What about crypto.randomUUID()?
Added to the Web Crypto spec in 2021 and now in every evergreen browser, crypto.randomUUID() returns a version 4 UUID string built from 122 bits of crypto.getRandomValues() output:
crypto.randomUUID();
// â '8f14e45f-ceea-467a-a7e3-6c2c9d0a0a17'
It is, byte for byte, the right way to mint a v4 UUID in client-side JavaScript. No more Math.random()-based UUID hacks copied from a 2014 Stack Overflow answer. No npm dependency. No 600-line UUID library.
The one limitation: crypto.randomUUID() only produces v4 UUIDs (random). If you need a v7 UUID â the time-ordered variant from RFC 9562 thatâs increasingly popular for database primary keys â you build it yourself on top of crypto.getRandomValues(). Our UUID Generator supports both, and runs entirely in your browser tab, so you can mint hundreds of v4 or v7 UUIDs without ever sending bytes to a server.
When Math.random is fine (and when it absolutely isnât)
Math.random() is not a security bug in itself â it is a tool with a specific job. Use it when the consequence of an attacker predicting the next value is nothing:
- Picking a random color for a UI gradient.
- Choosing which tip to show on an empty state.
- Jittering a polling interval to spread server load.
- Shuffling a list of marketing testimonials.
- Generating a random sample for a non-cryptographic A/B bucket (though prefer
crypto.getRandomValues()if the bucket assignment ever feeds analytics that affect product decisions).
Do not use Math.random() for any of these:
- Session tokens, JWT IDs, or refresh tokens.
- CSRF tokens.
- Password reset links or magic-link tokens.
- Passwords (even temporary ones â âtemp123!â is forever once itâs been generated).
- API keys or any string a user trusts as a credential.
- Cryptographic salts, nonces, or IVs.
- Game seeds in any game where money is on the line.
- UUIDs used as security boundaries (e.g. âonly people who know the URL can view this resourceâ).
- Random sampling in a study where bias matters.
A good rule from the senior dev playbook: if you find yourself wondering whether Math.random() is good enough, it isnât. Switch to crypto.getRandomValues(). The performance difference is a fraction of a microsecond. The reasoning difference is your weekend.
How a browser-based password generator uses entropy
When you click âGenerateâ on a client-side password generator, the entire flow happens in your tab. Here is roughly what runs:
function generatePassword(length, alphabet) {
const out = new Array(length);
const bytes = new Uint8Array(length);
// Pull length bytes of CSPRNG output from the OS via the browser.
crypto.getRandomValues(bytes);
// Use rejection sampling to avoid modulo bias.
for (let i = 0; i < length; i++) {
let b = bytes[i];
while (b >= alphabet.length * Math.floor(256 / alphabet.length)) {
const refill = new Uint8Array(1);
crypto.getRandomValues(refill);
b = refill[0];
}
out[i] = alphabet[b % alphabet.length];
}
return out.join('');
}
Three details that matter for browser random number generator security:
- The bytes never leave the tab. No network request, no server log, no cache. The password exists only in your browserâs memory until you copy it.
- Rejection sampling avoids modulo bias. If the alphabet has 70 characters, simply doing
b % 70would slightly favor the first 16 characters (since 256 isnât divisible by 70). The loop discards bytes that would skew the distribution. - Entropy is calculated from the alphabet size and length. A 16-character password drawn from a 94-character printable ASCII alphabet has
log2(94^16) â 105 bitsof entropy â enough to resist offline attacks for the foreseeable future, regardless of GPU cost curves.
You can verify all of this in your browserâs dev tools: open Network, generate a password, and watch nothing happen. Thatâs the entire point of doing it client-side.
Edge cases worth knowing
A handful of corners where browser randomness gets weird:
- Insecure contexts.
crypto.getRandomValues()works on HTTP, butcrypto.subtle(the rest of Web Crypto, includingcrypto.randomUUIDin some older builds) requires HTTPS orlocalhost. If youâre prototyping over HTTP, prefer to test onlocalhost. - WebViews and old embedded browsers. Android System WebView versions before 2019 had implementation bugs in early Chromium builds. If you ship to embedded devices, sanity-check the browser version.
- Server-side rendering. Donât call
crypto.getRandomValues()during SSR and re-use the value on the client â youâll either leak the same value to every visitor or hydrate-mismatch. Generate on the client, where the user is. - Web Workers.
crypto.getRandomValues()is available in workers. If you need a lot of randomness on a hot path, generate it in a worker and post it back. - Test environments.
Math.random()is reproducible if you mock it.crypto.getRandomValues()is harder to mock; usevi.spyOn(crypto, 'getRandomValues')(Vitest) or seam in a small helper in production code that you swap in tests.
A short checklist for your next code review
When a PR contains the word random, scan for:
- Any use of
Math.random()in a file path that includesauth,token,session,password, orcrypto. Flag it. - Any UUID generation library older than
crypto.randomUUID(). Replace it. - Any homemade âshuffleâ function used on a security-relevant list. Use FisherâYates with
crypto.getRandomValues(). - Any ârandom delayâ used to obscure timing. Thatâs not how timing-safe code works â use
crypto.subtle.timingSafeEqualor its equivalent. - Any seed value thatâs less than 128 bits. If an attacker can guess the seed, the rest is theater.
That checklist plus a working knowledge of where the bytes come from will catch ninety-five percent of randomness bugs before they ever reach production.
Try it yourself
If youâd like to see CSPRNG output without writing any code, our Password Generator and UUID Generator at Kestrel Tools both run entirely in your browser tab, pulling from crypto.getRandomValues() for every character. Open dev tools, watch the empty Network panel, and youâll see what âclient-sideâ actually means: the bytes start in the OS, end in your clipboard, and never touch a server in between.
Thatâs where random numbers come from in your browser. Now you can stop trusting the magic and start trusting the path.