iKit
Tutorial · 11 min read ·

How to Generate a UUID v4 (and When NOT to Use It)

Generate a UUID v4 in one line across browser, Node, Python, and CLI — and learn the three cases where UUID v4 is the wrong identifier to reach for.

How to Generate a UUID v4 (and When NOT to Use It)

How to Generate a UUID v4 (and When NOT to Use It)

You need a unique identifier for a row, a file, or a request trace. You search "uuid generator" and land on ten sites that all claim to produce random UUIDs. Some hand you a weak identifier from a broken PRNG; a few log every UUID to an analytics backend. This guide shows how to produce a cryptographically secure UUID v4 in every environment you'll touch — and the three places UUID v4 is the wrong answer.

What a UUID v4 actually is

A UUID (Universally Unique Identifier) is a 128-bit value, written as 32 hexadecimal digits separated into five groups: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. The format comes from RFC 4122, and the updated superset RFC 9562 published in May 2024 adds versions 6, 7, and 8. UUID v4 is the random variant: 122 bits of randomness with 6 fixed bits encoding the version and variant.

The anatomy of a v4 UUID

Here's a typical v4, with the structural bits called out:

f47ac10b-58cc-4372-a567-0e02b2c3d479
              ^     ^
              |     └── variant bits: 10xx (RFC 4122)
              └──────── version nibble: 4

Every UUID v4 will have a 4 as the first character of the third group, and the first character of the fourth group will always be 8, 9, a, or b. Everything else is random. If a generated string breaks those rules, it isn't a valid v4.

Version bits and variant bits explained

The 128 bits are split by the RFC as follows:

  • 48 bits of randomness (before the version nibble)
  • 4 bits for the version (always 0100 for v4)
  • 12 more bits of randomness
  • 2 bits for the variant (always 10 for RFC 4122)
  • 62 more bits of randomness

That leaves 122 usable random bits — enough that the universe is likely to burn out before you see a collision, provided the randomness is real.

Collision odds you can ignore

The birthday paradox tells us when to start worrying. For 2^122 possible values, the 50% collision threshold is roughly 2^61 — about 2.7 quintillion UUIDs. To put that in perspective, if you generated one billion UUIDs per second on every CPU core in every datacenter on Earth, you'd still need roughly 85 years to reach even a 1% chance of a single collision. This is why engineers treat UUID v4 collisions as mathematically impossible in practice — it's more likely that your hard drive silently flips a bit and corrupts the record first.

How to generate UUID v4 in every environment

You almost never need a library for this. Every modern runtime has a one-liner.

In the browser

The crypto.randomUUID() method is part of the Web Crypto API and ships in Chrome 92+, Firefox 95+, Safari 15.4+, and every evergreen Edge. It returns a secure v4 UUID string and is the only correct way to make one client-side.

const id = crypto.randomUUID();
console.log(id);
// "3f2a7b1c-8d4e-4b9c-a5f6-7e8d9c0b1a2f"

The function is synchronous, adds zero dependencies, and is backed by the same CSPRNG the browser uses for TLS. It's only available on secure contexts (HTTPS, localhost) — on an http:// page you'll hit TypeError: crypto.randomUUID is not a function.

In Node.js

Node 19 and later expose the same API on the global crypto object. For older Node you can import it explicitly:

// Node 19+
const id = crypto.randomUUID();

// Node 14.17+ compatible
const { randomUUID } = require("node:crypto");
const id2 = randomUUID();

Both paths call into OpenSSL's CSPRNG, so don't pull in the uuid package just for v4 — it's 12 kB on disk and slower than the built-in.

In Python

Python's standard library has shipped uuid since 2.5:

import uuid

new_id = uuid.uuid4()
print(new_id)
# UUID('a6c5f2e1-9b4d-4f8a-b3c2-8e1d7c5a4f0b')

print(str(new_id))
# 'a6c5f2e1-9b4d-4f8a-b3c2-8e1d7c5a4f0b'

uuid.uuid4() reads from /dev/urandom on Unix and CryptGenRandom on Windows, both cryptographically secure sources.

At the shell

Linux and macOS ship a kernel-level UUID source in /proc/sys/kernel/random/uuid (Linux) and uuidgen (both):

# macOS and most Linux distros
uuidgen
# F47AC10B-58CC-4372-A567-0E02B2C3D479

# Pure Linux
cat /proc/sys/kernel/random/uuid

Note that macOS's uuidgen returns uppercase by default. Pipe through tr '[:upper:]' '[:lower:]' if your API expects lowercase.

In a browser UI without writing code

If you need one or a hundred UUIDs to paste into a spreadsheet, staging data seed, or Postman request, the free iKit UUID Generator generates v4 UUIDs entirely in your browser with crypto.randomUUID(), copies to clipboard, and supports bulk export. Nothing ever leaves your tab.

When NOT to use UUID v4

This is the part most tutorials skip. UUID v4 is a general-purpose identifier, which means it's the right default for many things and the wrong default for three specific cases.

As a primary key in high-write OLTP tables

UUID v4 is fully random. When you insert 10,000 new rows per second, each row lands at a random position in the B-tree index, causing the database to split pages, dirty a fresh buffer, and flush more WAL than a sequential key would. On Postgres and MySQL with InnoDB, this fragmentation is measurable and painful past a few hundred million rows.

The fix isn't to avoid UUIDs — it's to use one that sorts by time. UUID v7 (from RFC 9562) encodes a 48-bit Unix millisecond timestamp in the first half:

0189f7d2-4a1c-7b8d-9e3f-2c4a6b8d0f1e
^^^^^^^^^^^^^       ^
timestamp ms        version 7

ULID does the same thing with a different base-32 alphabet. Both keep uniqueness but add index locality, so inserts stay clustered at the tail of the index.

For session tokens, API keys, or password reset codes

A UUID v4 has 122 bits of entropy, which is plenty, but it's also widely recognized as "just a random ID" — meaning tools, logs, and error messages treat it as low-sensitivity and may log it in places where a real secret shouldn't go. Worse, developers sometimes reach for UUID v4 in browser code, not realizing that only crypto.randomUUID() is secure — Math.random()-based implementations are still everywhere on NPM.

For anything that acts as a credential, use a purpose-built tool like the iKit Password Generator to produce high-entropy random strings, and treat them with the same care as any other secret.

For user-facing IDs

f47ac10b-58cc-4372-a567-0e02b2c3d479 is unreadable, unwritable, and impossible to remember. Nobody wants a support ticket that starts with "my order ID is ef87…". If the identifier will ever be spoken, typed, or read aloud, use a shorter human-readable format — squad names, incrementing numbers, or a 6-character base-32 code. Keep the UUID internal.

UUID v4 vs UUID v7, ULID, and Nanoid

Identifier Size Sortable by time Collision space Best for
UUID v4 128 bits No 2^122 General-purpose random IDs
UUID v7 128 bits Yes (ms) 2^74 Database primary keys (modern)
ULID 128 bits Yes (ms) 2^80 Database keys + lexical ordering
Nanoid (21) ~126 bits No ~2^126 Short URL-safe IDs (A-Za-z0-9_-)
Snowflake 64 bits Yes Per-worker Distributed systems with coordinator

For new projects picking a primary key today, UUID v7 is the right default. Stick with v4 when you explicitly want to leak no timing information — for example, surrogate keys on a table that logs authentication attempts.

Common mistakes that break UUID v4

Even with crypto.randomUUID() available everywhere, three failure modes keep showing up in production code.

Using Math.random() instead of a CSPRNG

The oldest mistake. Math.random() is not cryptographically secure and may be seeded from the process's start time. A well-known 2015 bug in early versions of the uuid NPM package produced predictable UUIDs because of this. If you see code that looks like this, rewrite it:

// DO NOT USE — biased and predictable
function badUuid() {
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
    const r = Math.random() * 16 | 0;
    const v = c === "x" ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

crypto.randomUUID() has been universally supported since 2022. There is no excuse for the above code in 2026.

Regenerating instead of storing

A common mistake in background jobs and retry logic is generating a new UUID on each attempt instead of passing the original along. This breaks idempotency — the downstream API can no longer deduplicate. Generate the UUID once, at the boundary where the job originates, and pass it through every retry.

Truncating UUID v4 to save space

"We only need 8 characters, so let's trim the UUID." At 8 hex characters you have 32 bits — a birthday collision becomes likely after only 65,000 values. If you need shorter, use a purpose-built short ID (Nanoid with a custom alphabet and length) instead of truncating a UUID.

Validating UUID v4 with a regex

To confirm a string is a well-formed v4, use the anchored pattern:

const 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;

UUID_V4.test("f47ac10b-58cc-4372-a567-0e02b2c3d479"); // true
UUID_V4.test("f47ac10b-58cc-1372-a567-0e02b2c3d479"); // false (version is 1)
UUID_V4.test("f47ac10b-58cc-4372-c567-0e02b2c3d479"); // false (variant is wrong)

This checks the version nibble (always 4) and the variant bits (first character must be 8, 9, a, or b). If you're parsing JSON payloads that contain UUIDs, the iKit JSON Decoder formats and validates nested structures so you can spot malformed IDs at a glance before the database rejects them.

Related on iKit

Related posts