UUID versions compared
How each UUID version is generated, why v7 is winning for database primary keys, when v4 is still the safe default, and the storage choices that make UUIDs cheap or expensive.
A UUID is 128 bits of identifier that doesn't need a central authority to hand it out. There are several ways to generate those 128 bits, each optimizing for something different. Picking the wrong version can wreck your database performance or leak information you didn't mean to.
The short answer
- UUID v4 — you need a random identifier and don't care about ordering. Safe default.
- UUID v7 — you're using it as a database primary key. Better index performance, chronological ordering, still practically unique.
- UUID v1 — you have a specific reason to embed MAC address + timestamp. Rarely the right choice today.
- UUID v5 — you need a deterministic UUID derived from a name (e.g. a URL). Niche but useful.
UUID v4 — random
122 bits of randomness, 6 bits fixed for the version and variant. Collision probability is astronomically low — you'd need to generate a billion per second for 85 years to have a 50% chance of one collision. Use crypto.randomUUID() or the UUID generator.
The catch: v4 UUIDs are unordered. Using them as a B-tree primary key makes every insert land at a random position in the index, causing page splits and killing insert throughput on large tables. Fine for small tables; painful past a few million rows.
UUID v7 — time-ordered
RFC 9562 (2024). The first 48 bits are a Unix millisecond timestamp, the remaining 74 bits are random. Result: UUIDs generated close in time sort close together, so inserts append to the end of the index instead of scattering — the same win as auto-increment integers, without the coordination.
0193d5f0-abcd-7abc-8def-123456789abc
└──timestamp──┘│└─rand─┘│└──random──┘
version variantIf you're picking a UUID version for a new database primary key today, v7 is almost always the right answer. Postgres 18 adds a native uuidv7() function; on older versions use a library like uuidv7 (JS), uuid-utils (Rust), or uuid7 (Python).
UUID v1 — timestamp + MAC address
The original time-based UUID. Embeds the machine's MAC address, which historically leaked the generating machine — a real privacy issue that helped identify malware authors. Also uses a 100-nanosecond Gregorian epoch, which is fiddly. Skip v1 unless you're maintaining an existing system that requires it. If you want time-ordering today, use v7.
UUID v5 — name-based (SHA-1)
Deterministic: uuidv5(namespace, name) always returns the same UUID for the same inputs. Useful when you need a stable identifier derived from something else — say, a UUID for every URL, every S3 object key, or every tenant slug. Same input always maps to the same UUID, no lookup table needed.
v3 is the same idea with MD5. Use v5 unless you have a compatibility reason to pick v3.
Storage — pick the right column type
- Postgres — native
UUIDtype (16 bytes). Never store UUIDs asTEXT; that's 36 bytes plus overhead and slower comparisons. - MySQL —
BINARY(16)for storage efficiency, with a helper function to format/parse. UUIDs asCHAR(36)also work but cost 2.25× the space. - SQLite —
BLOBof 16 bytes, orTEXTif you value readability more than size. - URLs — use base32 or base64url encoding to shorten from 36 to ~22 characters if URL length matters.
Common pitfalls
- Using UUIDs as public identifiers "for security". They're unguessable, but they're not authentication. Always authorize the caller separately.
- Assuming UUID v4 uniqueness across systems that reuse RNGs. If your seed is deterministic (some embedded systems, poorly initialized RNGs), you'll get collisions. Use the OS CSPRNG.
- Storing as strings when your DB has a UUID type. You pay in space, comparison speed, and index size. Every time.
- Exposing v1 UUIDs externally. Anyone can extract the timestamp and MAC address. Not what you want on a public API.
Decision tree
- Is this a database primary key? → v7.
- Do you need a stable UUID for a given input? → v5.
- Anything else where you just need a unique ID? → v4.