# How to Generate a Cryptographically Secure Random Number in Your Browser

> Math.random() is not secure. Learn why, what "secure" means for random number generation, the right browser API to use (crypto.getRandomValues), and how to generate a random number in a range without bias.

URL: https://uttir.com/blog/how-to-generate-a-cryptographically-secure-random-number
Published: 2026-08-21
Author: Uttir
Reading time: 5 min
Tags: security, random, cryptography, developer-tools, best-practices

## Quick answer

For a cryptographically secure random number in the browser, use `crypto.getRandomValues()` — the Web Crypto API, available in every modern browser. `Math.random()` is not secure: it is a PRNG, not a CSPRNG, and it is seeded with a value that is often predictable (timestamp, in some implementations). The [Uttir Random Number Generator](/random-number-generator) uses crypto.getRandomValues() in your browser, with options for cryptographically-secure mode, multiple numbers, and unique numbers (no duplicates).

You need a random number. You reach for `Math.random()`. It works. It returns a number between 0 and 1. You scale it to your range. You ship it.

The problem: `Math.random()` is not cryptographically secure. It is a pseudo-random number generator, not a cryptographically-secure pseudo-random number generator (CSPRNG). For most uses (a game, a UI effect, a sample dataset), the difference does not matter. For some uses (a password, a token, a security identifier, anything where predictability is a vulnerability), the difference is the bug that gets you breached.

This post is the short version of when it matters, why `Math.random()` is not enough, what to use instead, and how to do it right in the browser. The [Uttir Random Number Generator](/random-number-generator) uses the right API and exposes a "cryptographically secure" option; the [Password Generator](/password-generator) uses the same API for the same reason.

## Why Math.random() is not secure

`Math.random()` is a pseudo-random number generator (PRNG). It takes a seed value, runs a deterministic algorithm on it, and produces a sequence of numbers that look random. The numbers are not random; they are the output of a function. If you know the seed and the algorithm, you can predict every number the function will ever produce.

The seed in most JavaScript implementations is derived from the current time, in milliseconds. If the attacker knows roughly when the random number was generated (often, they do — server logs, request timestamps, observable side effects), they can narrow the seed to a small range, run the algorithm with each candidate seed, and predict the output.

For a CSPRNG, the seed is derived from a source of true entropy (hardware noise, OS-level randomness, user input) and the algorithm is designed so that even with full knowledge of the output, the attacker cannot predict the next number. The seed is not recoverable from the output; the output does not reveal the internal state.

The distinction is not academic. In 2012, a group of researchers hacked a large number of Bitcoin wallets by exploiting a similar weakness in the Java SecureRandom implementation on Android. The wallets generated random numbers using a PRNG that was not cryptographically secure, and the researchers could predict the private keys from a small number of outputs. The same class of bug, in any system, has the same class of consequence.

## What to use instead

In the browser, the right API is `crypto.getRandomValues()`. It is part of the Web Crypto API, available in every modern browser, and it is a CSPRNG. The output is unpredictable, the seed is not recoverable, and the implementation is audited by the browser vendors.

```
// Generate a 32-byte random buffer.
const buffer = new Uint8Array(32);
crypto.getRandomValues(buffer);

// Or, to get a single random number in a range:
function secureRandomInt(max) {
    const buffer = new Uint32Array(1);
    crypto.getRandomValues(buffer);
    return buffer[0] % max;
}
```

This is what the Web Crypto API is for. The implementation is in the browser, not in user-space code, so it is not vulnerable to the JavaScript-level attacks that `Math.random()` is. The output is the same kind of randomness that `/dev/urandom` on a Unix system or `BCryptGenRandom` on Windows provides — the kind that is suitable for cryptographic keys, session tokens, password salts, and any other security-sensitive use.

## The modulo bias problem

The naive `buffer[0] % max` in the example above is biased when the range of the random number is not a power-of-two divisor of the buffer's max. If the buffer is 32 bits (max value 2^32 = 4,294,967,296) and the desired max is 100, then 4,294,967,296 is not evenly divisible by 100. The first 96 values (0, 100, 200, ..., 429,496,700) get one extra hit each, and the bias is small but measurable.

For most uses (a game, a UI effect, a sample), the bias is so small it does not matter. For a security-sensitive use (a session token, a password salt, a cryptographic nonce), the bias is a vulnerability. The fix is to use a rejection-sampling loop: keep generating random numbers until you get one in the right range.

```
function secureRandomInt(max) {
    // Compute the largest multiple of max that fits in 32 bits.
    const limit = Math.floor(0xFFFFFFFF / max) * max;
    const buffer = new Uint32Array(1);
    let value;
    do {
        crypto.getRandomValues(buffer);
        value = buffer[0];
    } while (value >= limit);
    return value % max;
}
```

This eliminates the bias. The loop runs at most a few iterations (the expected number of iterations is 1 for small max, more for max close to 2^32). The performance cost is negligible.

The [Uttir Random Number Generator](/random-number-generator) does this for you — it accepts a min, max, and count, and uses the rejection-sampling approach to produce unbiased random numbers.

## When Math.random() is fine

For most uses, `Math.random()` is fine. The list of fine uses is long; the list of not-fine uses is short but important.

**Fine:**

- A game (a coin flip for fun, a random number in a board game, a random enemy spawn)

- A UI effect (a random particle direction, a random colour, a random delay)

- A sample dataset (a random subset of users for an A/B test, a random shuffle for a demo)

- A random ordering for display (a random sort of search results, a random order of gallery images)

**Not fine:**

- A password (use the [Password Generator](/password-generator))

- A session token (use crypto.getRandomValues to fill a buffer, base64-encode the result)

- A password reset token (same as session token)

- A CSRF token (same as session token)

- A cryptographic nonce (use crypto.getRandomValues, never Math.random)

- A UUID (use crypto.randomUUID, which is built on crypto.getRandomValues)

- A cryptographic key (use crypto.subtle.generateKey, which is built on crypto.getRandomValues)

The rule of thumb: if the random value is shown to the user, it is probably fine to use `Math.random()`. If the random value is sent to the server, used as a security token, or used to derive a cryptographic key, use `crypto.getRandomValues()` instead.

## Other browser APIs

Beyond `crypto.getRandomValues`, the Web Crypto API has a few other useful primitives.

**`crypto.randomUUID()`** returns a random UUID v4. It is the right way to generate a UUID in the browser; the implementation is built on `crypto.getRandomValues`, and the output is RFC 4122 compliant. The [Uttir UUID Generator](/uuid-generator) uses this internally.

**`crypto.subtle.generateKey()`** generates a cryptographic key for use with the Web Crypto API's encryption, decryption, signing, and verification functions. The output is a `CryptoKey` object that you can export, store, or use directly.

**`crypto.subtle.digest()`** computes a hash (SHA-256, SHA-384, SHA-512) of a buffer. Useful for fingerprinting, integrity verification, and content addressing. The [Password Strength Checker](/password-strength-checker) can use this to check if a password has appeared in known breach databases (by hashing the password and comparing against a list of hashes).

All four are part of the same Web Crypto API, available in every modern browser, and free to use. There is no excuse for shipping `Math.random()` in a security-sensitive code path.

## How to do it in your browser

The [Uttir Random Number Generator](/random-number-generator) accepts a min, max, and count, with a "cryptographically secure" option that uses `crypto.getRandomValues` with the rejection-sampling approach. The output is unbiased, fast, and never predictable from the previous output. The [Password Generator](/password-generator) uses the same API for the same reason. The [UUID Generator](/uuid-generator) uses `crypto.randomUUID()`, which is built on the same primitive.

The [Coin Flip](/coin-flip) tool is a good example of the same API used for a simpler case: a single bit of random, biased neither toward heads nor tails, with no way to predict the next flip from the previous one. The [Password Strength Checker](/password-strength-checker) is the right tool to verify that the password you generated (or the one you already have) is strong enough to survive a guessing attack.

## Related tools

- [Random Number Generator](https://uttir.com/random-number-generator) — Pick a random number, roll dice, or shuffle a list. Cryptographically secure, runs locally.
- [Password Generator](https://uttir.com/password-generator) — Create strong, random passwords with custom length and character sets — right in your browser.
- [UUID Generator](https://uttir.com/uuid-generator) — Generate cryptographically random UUID v4 identifiers, one or a thousand at a time.
- [Password Strength Checker](https://uttir.com/password-strength-checker) — See how strong a password really is — entropy, time-to-crack, and a list of issues, all computed in your browser.
- [Coin Flip](https://uttir.com/coin-flip) — Flip a virtual coin or roll dice. Cryptographically fair randomness, runs in your browser, history tracked per session.

---

For the full HTML article, visit https://uttir.com/blog/how-to-generate-a-cryptographically-secure-random-number.
This file is the markdown rendering at https://uttir.com/blog/how-to-generate-a-cryptographically-secure-random-number.md. See https://uttir.com/llms.txt for a site-wide summary.
