UUID Guide: v1 vs v4 vs v7, ULID Comparison, and Database Best Practices
Every distributed system eventually needs to generate IDs without a central coordinator. UUIDs (Universally Unique Identifiers) solve this: any node can generate an ID that will not collide with IDs produced by any other node, anywhere, at any time — with overwhelming probability. But not all UUID versions are equal. Choosing the wrong one can fragment your database indexes, leak timing information, or produce IDs that are hard to sort. Use the UUID Generator to follow along and generate IDs in any version covered here.
This guide covers everything from the bit-level anatomy of a UUID to the practical question of whether you should use UUID v4, v7, or ULID for your next database table. It also links to the generators guide for a broader look at random and structured ID generators.
UUID Anatomy: 128 Bits in 36 Characters
A UUID is a 128-bit value, conventionally displayed as 32 hexadecimal digits grouped into five sections separated by hyphens:
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
^ ^ ^ ^ ^
32 bits 16 16 16 48 bitsThe M nibble encodes the version (1–8). The top two bits of the N byte encode the variant. For all modern UUIDs (RFC 4122 and RFC 9562), the variant bits are 10 in binary, leaving 62 bits of the N group for data. That gives you a total of 122 bits of actual payload in a v4 UUID — the remaining 6 bits are fixed by the spec.
Example v4 UUID with version and variant bits highlighted:
550e8400-e29b-4137-a716-446655440000
^ ^
4 = version 4 (random)
a = 1010 in binary → variant 10xxUUID Versions Compared
Seven versions are standardized. RFC 4122 (2005) defined v1–v5. RFC 9562 (2024) added v6, v7, and v8, standardizing formats that had existed as drafts for years.
| Version | Source of uniqueness | Sortable? | RFC | Typical use |
|---|---|---|---|---|
| v1 | 60-bit UTC timestamp + MAC address + clock sequence | Partially (time in wrong byte order) | RFC 4122 | Legacy systems, Cassandra |
| v3 | MD5 hash of a namespace UUID + name | No | RFC 4122 | Deterministic IDs from names (legacy) |
| v4 | 122 bits of cryptographically random data | No | RFC 4122 | General purpose, most widely used |
| v5 | SHA-1 hash of a namespace UUID + name | No | RFC 4122 | Deterministic IDs from names (preferred over v3) |
| v6 | Reordered v1 timestamp (time-high first) + MAC + clock | Yes | RFC 9562 | Drop-in sortable replacement for v1 |
| v7 | 48-bit Unix millisecond timestamp + 74 bits of random | Yes | RFC 9562 | Database primary keys, modern systems |
| v8 | Custom — vendor-defined layout | Depends | RFC 9562 | Application-specific schemes |
v3 vs v5: Both produce deterministic UUIDs from a (namespace, name) pair. v5 uses SHA-1 instead of MD5 — prefer v5 for any new work. Neither is cryptographically secure as a MAC; they are identity functions, not authentication tokens.
v6 vs v7: v6 reorders the v1 timestamp bytes to make it lexicographically sortable while keeping the MAC address component. v7 replaces the MAC with random bits and uses the simpler Unix epoch milliseconds rather than the 100-nanosecond intervals counted from October 15, 1582. For new systems, v7 is the right choice.
Why UUID v7 Wins for Databases
The dominant database index structure is a B-tree. B-trees maintain sorted order: new entries go to their sorted position, and the tree rebalances as needed. When UUIDs are random (v4), every insert lands at a random position in the index. This causes two problems at scale:
- Page splits: Inserting in the middle of a full B-tree page forces a page split — the database must allocate a new page, copy half the entries, and update parent pointers. At high insert rates, this becomes a significant overhead.
- Cache thrashing: With v4 UUIDs, inserts touch random pages throughout the index. Even a moderately large table will not fit in the buffer pool. Every insert becomes a disk read before the write — cache hit rate for the index drops near zero.
UUID v7 inserts are nearly always appended to the rightmost leaf of the B-tree because the timestamp prefix increases monotonically. The database only keeps the last few leaf pages warm in cache. Page splits become rare. Insert throughput with v7 primary keys can be 3–5x higher than with v4 on a table with hundreds of millions of rows — the exact figure depends on row size, hardware, and database engine, but the directional advantage is consistent across PostgreSQL, MySQL/InnoDB, and SQL Server benchmarks.
PostgreSQL 17 added a native uuidv7() function. MySQL does not have one yet, but you can store v7 UUIDs as BINARY(16) and generate them in application code. Avoid storing UUIDs as VARCHAR(36) — it wastes space and slows index comparisons.
-- PostgreSQL 17+
SELECT uuidv7();
-- 018f3c2a-1b7e-7a3d-b4c2-9f1e2d3a4b5c
-- Earlier PostgreSQL: generate in app, store as UUID type
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- v4, swap for app-generated v7
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
payload JSONB
);
-- MySQL: store as BINARY(16) for space efficiency
CREATE TABLE events (
id BINARY(16) PRIMARY KEY,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
payload JSON
);ULID: The Alternative to UUID v7
ULID (Universally Unique Lexicographically Sortable Identifier) was proposed in 2016, before UUID v7 was standardized, to solve the same database-index problem. Understanding it helps you choose between the two.
A ULID is 128 bits, encoded as 26 Crockford Base32 characters:
01ARZ3NDEKTSV4RRFFQ69G5FAV
^ ^^^^^^^^^^^^^^^^^^
10 chars 16 chars
48-bit 80-bit
timestamp randomness
(ms unix) (CSPRNG)| Property | UUID v7 | ULID |
|---|---|---|
| Bit width | 128 | 128 |
| Timestamp bits | 48 (Unix ms) | 48 (Unix ms) |
| Random bits | 74 | 80 |
| String length | 36 chars (with hyphens) | 26 chars |
| Alphabet | Hex (0–9, a–f) | Crockford Base32 (0–9, A–Z minus I, L, O, U) |
| Lexicographic sort | Yes (same as temporal order) | Yes (same as temporal order) |
| RFC standard | RFC 9562 | Community spec (not IETF) |
| Database type support | Native UUID type in PostgreSQL, MySQL | Typically stored as CHAR(26) or BINARY(16) |
| URL-safe | No (contains hyphens) | Yes |
The practical difference between 74 and 80 random bits is negligible for collision resistance (both are astronomically safe). The more important difference is ecosystem support: UUID v7 has an RFC, native database types, and growing library support in every language. ULID has no IETF standard and is less commonly supported natively.
Choose ULID when: you need short, URL-safe IDs and your stack already uses ULID libraries. Choose UUID v7 when: you want RFC-backed standardization, native database type storage, and broad language library support.
Collision Probability Math
UUID v4 has 122 bits of randomness (6 bits are fixed by the version and variant). By the birthday paradox, the probability of at least one collision when generating n UUIDs is approximately:
P(collision) ≈ 1 - e^(-n²/(2 × 2^122))
At n = 1 billion (10^9):
P ≈ 1 - e^(-10^18 / (2 × 5.3 × 10^36))
P ≈ 1.2 × 10^-19 (essentially zero)
To reach P = 50% (expected first collision):
n ≈ sqrt(ln(2) × 2^122)
n ≈ 2.71 × 10^18 (2.71 quintillion UUIDs)In practice: if you generate one UUID per millisecond, it takes about 86 million years to reach 2.71 quintillion. Collision is not a practical concern for v4.
UUID v7 has 74 random bits, making the collision space smaller than v4. But the timestamp prefix means collisions can only occur within the same millisecond:
Within one millisecond, 74 random bits → 2^74 ≈ 18.9 quadrillion possible values
Expected first collision within one ms: ~sqrt(ln(2) × 2^74) ≈ 3.6 billion UUIDs/ms
Realistic peak: a large system might generate 10,000 UUIDs/ms
At 10,000/ms: P(collision in 1ms) ≈ (10,000)² / (2 × 2^74) ≈ 2.6 × 10^-15Even at 10,000 IDs per millisecond, the collision probability per millisecond is effectively zero. If your system generates millions of IDs per millisecond, consider a monotonic counter in the random field (the spec allows this) or use a central sequence generator.
When collisions actually matter: They don't, for UUIDs, unless you are using a broken random number generator. The risk is not mathematical — it's implementation. A seeded PRNG (not a CSPRNG), a VM cloned without re-seeding, or a misconfigured container can produce predictable or repeated values. Use the platform's cryptographically secure RNG.
Security Considerations
UUID versions differ significantly in what information they leak:
- v1 leaks MAC address and timestamp. A v1 UUID reveals the network interface address of the generating machine and the exact time of generation down to 100 nanoseconds. Never expose v1 UUIDs in public-facing APIs if you need to protect server identity or generation timing.
- v7 leaks the millisecond timestamp. Anyone with a v7 UUID knows when it was generated, to the nearest millisecond. For most applications this is acceptable — row creation time is often public or low-sensitivity. But if the generation timestamp is a secret (e.g., it reveals when a user signed up or when a transaction occurred), use v4 instead.
- v4 leaks nothing — if generated correctly. A v4 UUID from a CSPRNG reveals no information about the generator, the time, or other generated values. But if your runtime uses a weak PRNG, an attacker may be able to predict future UUIDs from observed ones. This has been exploited in real applications (PHP's
mt_rand()-based UUID generation before PHP 7 was a known vulnerability class). - v3/v5 are deterministic and reversible by brute force. If the name space is known and the name is low-entropy (e.g., a sequential integer), an attacker can enumerate all possible UUIDs. Do not use v3/v5 as opaque tokens.
Never use UUIDs as authentication tokens or secrets. A UUID is an identifier, not a credential. Use a dedicated secret generator (e.g., crypto.randomBytes(32) encoded as base64url) for session tokens, API keys, and password reset links.
Code Examples
Node.js — built-in (v4)
// Node 14.17+: native crypto.randomUUID() — no dependency needed
const id = crypto.randomUUID();
// '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
// Works in browsers too (Web Crypto API)
const id2 = globalThis.crypto.randomUUID();Node.js — uuid package (v4, v5, v7)
npm install uuidimport { v4 as uuidv4, v5 as uuidv5, v7 as uuidv7 } from 'uuid';
// v4: random
const id4 = uuidv4();
// '110e8400-e29b-41d4-a716-446655440000'
// v5: deterministic from namespace + name
const DNS_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
const id5 = uuidv5('example.com', DNS_NAMESPACE);
// Always produces the same UUID for the same inputs
// v7: timestamp + random (preferred for database PKs)
const id7 = uuidv7();
// '018f3c2a-1b7e-7a3d-b4c2-9f1e2d3a4b5c'Python
import uuid
# v4: random
id4 = uuid.uuid4()
print(id4) # UUID('a8098c1a-f86e-11da-bd1a-00112444be1e')
# v5: deterministic
id5 = uuid.uuid5(uuid.NAMESPACE_DNS, 'example.com')
print(id5) # UUID('cfbff0d1-9375-5685-968c-48ce8b15ae17')
# v7: requires Python 3.12+
import uuid
id7 = uuid.uuid7() # Python 3.12+
print(id7)
# Earlier Python: use the uuid6 package
# pip install uuid6
import uuid6
id7 = uuid6.uuid7()
print(id7)PostgreSQL
-- v4 (available since PostgreSQL 9.4 via pgcrypto extension)
SELECT gen_random_uuid();
-- a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
-- v7 (native, PostgreSQL 17+)
SELECT uuidv7();
-- 018f3c2a-1b7e-7a3d-b4c2-9f1e2d3a4b5c
-- Extract timestamp from a v7 UUID
SELECT to_timestamp(
(('x' || translate(split_part('018f3c2a-1b7e-7a3d-b4c2-9f1e2d3a4b5c', '-', 1) ||
left(split_part('018f3c2a-1b7e-7a3d-b4c2-9f1e2d3a4b5c', '-', 2), 4),
'-', ''))::bit(48)::bigint) / 1000.0
);Rust
# Cargo.toml
uuid = { version = "1", features = ["v4", "v7"] }use uuid::Uuid;
use std::time::SystemTime;
fn main() {
// v4: random
let id4 = Uuid::new_v4();
println!("{}", id4);
// v7: timestamp + random
let ts = uuid::Timestamp::now(uuid::NoContext);
let id7 = Uuid::new_v7(ts);
println!("{}", id7);
}Choosing a Version: Decision Guide
Work through these questions in order to pick the right UUID version:
- Do you need a deterministic ID from a known name? Use v5 (SHA-1 namespace hash). Example: stable IDs for DNS names, URLs, or product SKUs where the same input must always produce the same ID.
- Do you need a database primary key and want good index performance? Use v7. It is monotonically increasing within a millisecond, which keeps B-tree indexes compact and avoids page splits. PostgreSQL 17+ has native support; for earlier versions, generate in application code.
- Do you need sortability but must avoid exposing a timestamp? This is a genuine tension. UUID v4 gives no temporal signal. If you need opaque but sortable IDs, consider encrypting a v7 UUID with a format-preserving cipher (FPE), or use a separate non-sequential public ID alongside an internal v7.
- Do you need pure randomness with no timestamp leakage? Use v4. Ensure your runtime uses a CSPRNG (it does in Node 14.17+, Python, Go, Rust, and modern PHP).
- Are you replacing a v1-based legacy system and need sortability? Use v6 as a drop-in replacement — same data sources as v1, bytes reordered for lexicographic sort.
- Do you need short, URL-safe IDs with the same sort properties as v7? Consider ULID (26 chars vs 36 for UUID). Trade-off: no RFC standard, less native database support.
| Need | Recommended | Avoid |
|---|---|---|
| Database primary key | v7 | v4 (index fragmentation at scale) |
| Deterministic from name | v5 | v3 (MD5, deprecated) |
| Pure random, no timestamp | v4 | v1 (leaks MAC + timestamp) |
| Short, URL-safe sortable ID | ULID | v4 (not sortable), v7 (hyphens) |
| Legacy v1 replacement | v6 | v1 (privacy concerns) |
| Authentication token / secret | crypto.randomBytes(32) | Any UUID version |
Generate UUID v4, v7, and other versions directly in your browser with the UUID Generator — no server, no data leaving your machine. For a broader look at ID and data generators available in DevToys Pro, see the generators guide.