Why You Should Always Use crypto.randomUUID() — UUID Collision Odds vs. Predictability
Code that generates a UUID v4 splits into two camps: code that uses the browser's Web Crypto API, like crypto.randomUUID(), and code that assembles a hex string by hand using Math.random(). Both produce the same-looking 8-4-4-4-12 string, so they look identical at a glance — but they differ fundamentally in "quality of randomness." Let's start by separating two questions that are often conflated: collision probability and predictability.
1. Collision probability: v4 is effectively nothing to worry about
Of UUID v4's 128 bits, 122 are randomly filled once you exclude the version (4 bits) and variant (2 bits) fields. The odds that this many possible values happen to collide are calculated with the "birthday problem" — the same phenomenon where the chance of two people sharing a birthday rises faster than intuition suggests as the group grows. Generating N values, the collision probability is roughly proportional to N²/2¹²³. Even generating 1 billion UUID v4s per second continuously, it takes about 85 years to reach a 50% collision probability. In short, v4's collision probability itself isn't a practical concern regardless of whether you use a CSPRNG. The performance issue covered in another guide on this site — "why using UUID as a DB primary key slows things down" — is a separate matter, caused by random sort order triggering B-tree index page splits, not by collisions.
2. The real risk isn't collisions — it's predictability
If collisions aren't the concern, why emphasize CSPRNGs at all? Because UUIDs are frequently used as values that "must be unguessable" — session tokens, password-reset links, share-only URLs. In that context, the real threat isn't "the odds this UUID happens to match another user's" — it's "can an attacker compute a specific UUID in advance?" Math.random() is a PRNG (pseudorandom number generator) that was never designed with cryptographic security as a goal; its internal seed has a narrow state space, and some engine implementations are known to have vulnerabilities that let an attacker work backward from the seed or prior outputs to future values. Build a UUID from that, and what you get "looks like a UUID in format but has far weaker security strength as a random value." crypto.randomUUID() and crypto.getRandomValues(), by contrast, draw from an OS-level CSPRNG (cryptographically secure pseudorandom number generator), designed so that no matter how many prior outputs an attacker collects, the next value can't be predicted.
const u = crypto.randomUUID();. A common anti-pattern seen in hand-rolled implementations, by contrast, looks like this:
| Approach | Randomness source | Fit for security use |
|---|---|---|
crypto.randomUUID() | OS CSPRNG | Suitable for session tokens, secret URLs |
crypto.getRandomValues() | OS CSPRNG | Suitable even for custom formats |
Hand-assembled from Math.random() | Non-cryptographic PRNG | Only non-security uses, like a throwaway UI key |
3. When either approach is fine
Not every UUID is security-sensitive. For a React list's key prop, a temporary DOM id, or a local test fixture — uses where "being guessed causes zero harm" — a Math.random()-based library carries no real risk. Conversely, for session IDs, email verification tokens, file-share links, and API keys — uses where a third party guessing the value causes real damage — you must use a CSPRNG-based generation method. The deciding question isn't "is the collision probability low," it's "does it matter if someone predicts this value?"
4. Browser compatibility and fallback pitfalls
crypto.randomUUID() is a relatively recent API, so it may be unavailable in older browsers or in non-HTTPS (HTTP) contexts, since the Web Crypto API only works in secure contexts. Code that falls back to Math.random() in that case is fairly common — and when that happens, the app can appear to work fine on the surface while its security strength has quietly dropped. If you need a fallback, the safe approach is to pull bytes directly from crypto.getRandomValues() and assemble them into RFC 4122 format yourself — that's still a CSPRNG source, so no predictability problem arises.
Frequently Asked Questions
Q. Is UUID v4's chance of an accidental collision worth worrying about in practice?
No. Thanks to its 122-bit random space, even generating 1 billion per second would take about 85 years to reach a 50% collision probability. At typical service scale, this probability is effectively negligible.
Q. Then why is a UUID built from Math.random() risky?
The problem isn't collision probability, it's predictability. Math.random() is a PRNG that wasn't designed for cryptographic security, so in some cases the next value can be guessed from the seed or prior outputs. Use it for a session token or secret URL and an attacker could guess the value and gain access.
Q. Which approach does this site's UUID Generator use?
The UUID Generator uses crypto.randomUUID(). The generated value is never sent outside the browser, and because it's backed by an OS-level CSPRNG, it's safe to use even for security-sensitive purposes.
Q. Is the "UUID as DB primary key" performance issue the same as the security issue in this guide?
No, they're different. The performance issue is a separate matter where random sort order triggers B-tree index page splits; this guide covers predictability and security strength as determined by the random-number-generation method. The DB performance issue is covered in a separate guide.