Generate secure UUIDs natively in JavaScript
Math.random() is unsuitable for IDs, since it relies on a predictable pseudo-random generator. crypto.randomUUID() delivers genuine UUID v4 values from a cryptographically secure random generator, with no external dependencies, equally in the browser and in Node.js.
Table of Contents
- 1. Why UUIDs, and why cryptographically secure?
- 2. UUID v4: structure and format
- 3. crypto.randomUUID(): the native method
- 4. How crypto.randomUUID() works internally
- 5. crypto.randomUUID() vs. Math.random() hacks
- 6. crypto.randomUUID() in Node.js
- 7. Practical applications: IDs, tokens, and deduplication
- 8. Collision probability and scaling
- 9. crypto.randomUUID() vs. UUID libraries compared
- 10. Summary
- 11. FAQ
1. Why UUIDs, and why cryptographically secure?
Unique identifiers are ubiquitous in modern web apps: temporary IDs for optimistic UI rendering, idempotency tokens for API requests, session nonces, correlation IDs for distributed logging, database keys when data is created on the client. The basic requirement profile is always the same: the ID must be unique enough that collisions do not occur in practice, and it must not be predictable, so attackers cannot guess or enumerate IDs.
crypto.randomUUID() satisfies both requirements. It uses a cryptographically secure pseudo-random number generator (CSPRNG) provided by the operating system, the same one used for cryptographic keys. That is a fundamental difference from Math.random(), which uses a deterministic algorithm that is fully predictable once the seed is known. For IDs that carry no security function, predictability is academic. But for tokens, nonces, and IDs that are treated as a secret, crypto.randomUUID() is the correct tool, and it takes only a single line of code.
2. UUID v4: structure and format
A UUID (Universally Unique Identifier) per RFC 4122 is a 128-bit number represented in a standardized text format: eight hexadecimal characters, followed by three groups of four characters, and finally twelve characters, separated by hyphens, 36 characters in total. The format is xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx, where M encodes the version (always 4 for v4) and the first two bits of N encode the variant (always binary 10 for RFC 4122, meaning 8, 9, a, or b in hex).
UUID v4 uses 122 of the 128 bits for random values, the remaining 6 bits are reserved for version and variant. That yields 2^122 possible values, over 5 quintillion distinct UUIDs. crypto.randomUUID() generates exactly this format: the method selects 122 random bits from the CSPRNG, sets the four version bits to 0100 and the two variant bits to 10, and formats the result as a standardized UUID string. The result is fully RFC-4122-compliant and interoperable with every system that expects UUID v4.
// crypto.randomUUID() (available in all modern browsers and Node.js 14.17+)
const id = crypto.randomUUID();
// Example output: '550e8400-e29b-41d4-a716-446655440000'
// Format: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx
console.log(id.length); // Always 36 characters
console.log(id[14]); // Always '4' (version 4)
console.log('89ab'.includes(id[19])); // Always true (RFC 4122 variant)
// Verify UUID v4 format with regex
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
console.log(UUID_V4_REGEX.test(crypto.randomUUID())); // Always true
// Generate multiple UUIDs
const ids = Array.from({ length: 5 }, () => crypto.randomUUID());
// Every call produces a unique, cryptographically random ID
3. crypto.randomUUID(): the native method
crypto.randomUUID() is a synchronous method on the global crypto object: no promise, no callback. It returns a UUID string directly. That is a deliberate design decision: generating a UUID from a CSPRNG is so fast (microseconds) that asynchronicity would offer no benefit while making the code unnecessarily complicated. The method has been available since Chrome 92, Firefox 95, Safari 15.4, and Node.js 14.17, with near-complete browser coverage.
One important detail about crypto.randomUUID(): like other secure-context APIs, it is only available on HTTPS pages and localhost in modern browsers. On HTTP pages, crypto.randomUUID may still be present (this depends on the browser and its configuration), but the secure-context requirement of the Web Crypto API suggests always checking the security context. In Node.js and Deno, crypto.randomUUID() is available without restriction, since there is no browser security model involved.
4. How crypto.randomUUID() works internally
Internally, crypto.randomUUID() calls the operating system's CSPRNG: /dev/urandom on Linux, BCryptGenRandom on Windows, arc4random on macOS. These sources draw on hardware entropy such as CPU timing jitter, interrupt timing, and (on modern CPUs) the hardware random generator via the RDRAND instruction. The resulting random stream is cryptographically secure, meaning that even with knowledge of every previously generated UUID, the next one cannot be predicted.
The implementation in V8 (the JavaScript engine in Chrome and Node.js) internally uses the same random generator as crypto.getRandomValues(), the low-level method of the Web Crypto API. crypto.randomUUID() is essentially a convenient wrapper that fetches 16 random bytes via getRandomValues, sets the version and variant bits, and formats the result as a UUID string. Anyone who wants to understand a custom UUID implementation can reproduce that with two lines of code, but in practice you should always use crypto.randomUUID() directly, since it is optimized inside the JavaScript engine.
// Understanding crypto.randomUUID() internals
// This manual implementation mirrors what the native method does
function manualUuidV4() {
// Get 16 cryptographically random bytes
const bytes = crypto.getRandomValues(new Uint8Array(16));
// Set version bits (4 = 0100) at byte index 6
bytes[6] = (bytes[6] & 0x0f) | 0x40;
// Set variant bits (RFC 4122 = 10xx) at byte index 8
bytes[8] = (bytes[8] & 0x3f) | 0x80;
// Format as UUID string: 8-4-4-4-12
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
// Always prefer the native method over manual implementation
const native = crypto.randomUUID(); // Optimized, maintained by browser
const manual = manualUuidV4(); // Equivalent result, but redundant
console.log(native); // e.g. 'a2f8b3c4-e1d7-4f56-89ab-c3d2e1f09876'
console.log(manual); // Same format, different random value
5. crypto.randomUUID() vs. Math.random() hacks
Before crypto.randomUUID() existed, the web was full of UUID implementations based on Math.random(). The best-known snippet ('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, fn)) produces visually correct UUIDs, but uses Math.random() as its entropy source. Math.random() is not a CSPRNG: in V8 it uses the xorshift128+ algorithm, which is fully predictable once the internal state is known. For UI IDs with no security relevance, that is acceptable. For anything that serves as a secret token, nonce, or session ID, it is dangerous.
The practical difference: IDs generated with crypto.randomUUID() cannot be predicted by observing multiple past IDs. With Math.random()-based IDs, it is theoretically possible to reconstruct the internal state of the PRNG from a sequence of UUIDs and predict future IDs. In an attack scenario where an attacker can observe several generated IDs through an XSS vulnerability, that would be a real risk. crypto.randomUUID() eliminates that risk structurally.
6. crypto.randomUUID() in Node.js
In Node.js, crypto.randomUUID() has been available through the built-in crypto module since version 14.17.0. Since Node.js 19, crypto is also available as a global object (as in the browser), so crypto.randomUUID() can be called without an import. For older Node.js versions, an import is required: const { randomUUID } = require('crypto') (CommonJS) or import { randomUUID } from 'node:crypto' (ESM). The node: prefix has been available since Node.js 14.18 and is explicitly recommended, since it clearly signals that this is a built-in module and prevents shadowing by npm packages with the same name.
In distributed Node.js applications, where many processes or pods call crypto.randomUUID() concurrently, collisions are practically excluded. The UUID v4 space with 2^122 possible values is so large that even at a million UUIDs per second across a thousand pods, the collision probability remains astronomically low over a hundred-year period. That makes crypto.randomUUID() a reliable primary key generator for client-created database entries without a centralized ID generator.
// Node.js: import crypto.randomUUID, prefer the node: prefix
import { randomUUID } from 'node:crypto';
// Express route: generate idempotency token
app.post('/api/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'] ?? randomUUID();
// Check if request was already processed
const existing = await db.orders.findOne({ idempotencyKey });
if (existing) {
return res.status(200).json(existing);
}
const order = await db.orders.create({
id: randomUUID(), // Primary key, generated client-side
idempotencyKey,
...req.body,
createdAt: new Date(),
});
res.status(201).json(order);
});
// Batch ID generation, all unique, no collision risk
const batchIds = Array.from({ length: 1000 }, randomUUID);
// Correlation ID for distributed tracing
function createRequestContext() {
return {
requestId: randomUUID(),
traceId: randomUUID(),
spanId: randomUUID().replace(/-/g, '').slice(0, 16), // 16-char span ID
};
}
7. Practical applications: IDs, tokens, and deduplication
crypto.randomUUID() solves several concrete problems in web development. First: temporary IDs for optimistic UI rendering. When a user submits a form, you want to show the new entity in the UI immediately, before the server response arrives. That requires an ID that can be generated client-side and either matches the server ID or gets replaced by it. Second: idempotency tokens for API requests. For HTTP requests that are not idempotent (POST, PATCH), you send a one-time token in the header that the server uses for duplicate detection.
Third: request deduplication in offline-first apps. When a user performs an action that gets stored offline and synchronized later, every action needs a unique ID so the server can detect duplicate synchronizations. crypto.randomUUID() is ideal for this: fast enough to be called synchronously inside an event handler, secure enough that no collisions occur with other clients working in parallel. Fourth: correlation IDs for frontend logging. Every API request gets a unique ID that appears in both the frontend log and the server log, enabling debugging in distributed systems.
8. Collision probability and scaling
The collision probability of crypto.randomUUID() is based on the birthday paradox: for n generated UUIDs drawn from a space of 2^122 possible values, the probability of a collision is approximately n² / (2 × 2^122). Concretely: to reach a 50% collision probability, you would need to generate roughly 2.7 × 10^18 UUIDs, that is 2.7 billion billion. An application generating a million UUIDs per second would need 85 million years to get there. In practice, the collision probability is negligible for every realistic use case.
Still, there are scenarios where UUID v4 should be complemented by more specialized formats. Sortability is a common requirement in databases: UUID v4 has no inherent temporal ordering, which can lead to B-tree fragmentation with large datasets. Formats such as ULID (Universally Unique Lexicographically Sortable Identifier) or UUID v7 (which includes a timestamp as a prefix) solve this problem. There is no native browser API for these formats, so an external library is genuinely required here. crypto.randomUUID() is the right choice whenever sortability is not a requirement.
| Method | Security | Sortable | Dependency |
|---|---|---|---|
| crypto.randomUUID() | CSPRNG, very high | No (random) | None (native) |
| Math.random() UUID hack | PRNG, predictable | No | None |
| uuid npm (v4) | CSPRNG internally | No | npm package needed |
| ULID | CSPRNG internally | Yes (time prefix) | npm package needed |
| UUID v7 | CSPRNG internally | Yes (time prefix) | npm package needed |
9. crypto.randomUUID() vs. UUID libraries compared
The npm package uuid (over 80 million downloads per week) was long the standard for UUID generation in JavaScript. Now that crypto.randomUUID() is available natively, it becomes redundant for the plain UUID v4 use case. The difference lies in dependency size (the uuid package is minimal, but not zero), compatibility (uuid also supports UUID v1, v3, v5), and handling of older environments. If the project already assumes Node.js 14.17+ and modern browsers, crypto.randomUUID() is the better choice: no external dependency, no package vulnerabilities, no version conflicts.
For teams that need sortability, or other UUID versions (v1 for time-based IDs, v5 for name-based deterministic IDs), the uuid library or specialized alternatives such as ulid or nanoid remain worthwhile. nanoid is a particularly interesting alternative to crypto.randomUUID(): it also uses the CSPRNG, but generates shorter, URL-safe IDs that are more compact in URLs, filenames, and display. The choice between crypto.randomUUID() and nanoid depends on whether RFC 4122 compliance is a requirement.
Mironsoft
Secure JavaScript architectures and modern web APIs
Security gaps from Math.random() in your app?
We audit existing code for insecure random generators and migrate it to crypto.randomUUID() and the Web Crypto API, for IDs, tokens, and nonces that meet real cryptographic standards.
Security audit
Identify Math.random() usages and assess risk potential
Migration
Secure replacement with crypto.randomUUID() and getRandomValues()
Architecture
ID strategies for distributed systems: UUID v4, ULID, or UUID v7
10. Summary
crypto.randomUUID() is the simplest and safest way to generate UUID v4 in modern JavaScript, with no external dependencies, no boilerplate, and no security compromises. The method is synchronous, guarantees RFC-4122-compliant UUIDs from a cryptographically secure random generator, and is equally available in the browser and Node.js. For every use case where UUID v4 is sufficient, temporary IDs, idempotency tokens, correlation IDs, optimistic UI rendering, crypto.randomUUID() is the right choice.
Choosing external libraries such as uuid, ulid, or nanoid is justified when specific requirements exist: sortability (ULID, UUID v7), shorter IDs (nanoid), other UUID versions (v1, v3, v5 via uuid), or compatibility with very old browser versions. In every other case, crypto.randomUUID() is the superior choice: one line of code, no dependencies, maximum security. The Math.random() UUID snippet that has been copied from Stack Overflow answers for ten years no longer has a place in new code.
crypto.randomUUID(): the essentials at a glance
Availability
Chrome 92+, Firefox 95+, Safari 15.4+, Node.js 14.17+. In Node.js: import { randomUUID } from 'node:crypto'.
Security
Operating system's CSPRNG. Not predictable, not derivable from past values. Fully replaces Math.random()-based UUID hacks.
Format
RFC-4122-compliant. 36 characters, 32 hex plus 4 hyphens. Version byte: always 4. Variant byte: always 8, 9, a, or b.
When to use an external library?
For sortability (ULID/UUID v7), shorter IDs (nanoid), or other UUID versions (uuid-npm). Otherwise always crypto.randomUUID().
11. FAQ: JavaScript crypto.randomUUID()
1What is crypto.randomUUID()?
2Why is Math.random() unsafe for UUIDs?
3UUID v4 vs. UUID v7?
4How to import in Node.js?
import { randomUUID } from 'node:crypto' (ESM) or const { randomUUID } = require('crypto'). Globally available since Node.js 19.5Can a collision occur?
6Still need uuid npm?
7Available on HTTP pages?
8Synchronous or asynchronous?
9Difference from nanoid?
10How to validate a UUID v4?
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, checks format, version byte, and variant byte.