Understand the differences between UUID v1, v4, v5, and ULID — when to use each, how they're generated, and best practices for database primary keys.
UUIDs are everywhere — database primary keys, API resources, session IDs. But not all UUIDs are created equal. Choosing the wrong version can hurt database performance, leak information, or cause collisions. Here's what every developer needs to know.
A UUID (Universally Unique Identifier) is a 128-bit number, typically displayed as 32 hex digits in 5 groups:
550e8400-e29b-41d4-a716-446655440000
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
^ ^
| variant (8, 9, a, b)
version (1-8)
The probability of generating a duplicate v4 UUID is astronomically small — you'd need to generate 1 billion UUIDs per second for 86 years to have a 50% chance of a single collision.
77f4fbe0-d3f6-11ef-9cd2-0242ac120002
^^
v1
Generated from: current timestamp (100ns intervals since Oct 15, 1582) + MAC address of the generating machine.
Pros: Monotonically increasing (good for B-tree indexes), can extract creation time.
Cons: Leaks the MAC address (privacy risk), not truly random, requires coordination in distributed systems to avoid collisions.
Use when: You need time-sortable IDs and are on a single machine or have a UUID service.
550e8400-e29b-41d4-a716-446655440000
^
4 = version 4
Generated from: 122 bits of cryptographically random data.
Pros: Simple, widely supported, no information leakage.
Cons: Completely random — bad for database index performance (random writes cause page splits in B-trees).
Use when: You need a unique ID and database insert pattern doesn't matter (or you're using a non-relational DB).
// Node.js — built-in since v14.17
const { randomUUID } = require('crypto');
const id = randomUUID(); // '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
// Browser
const id = crypto.randomUUID();
a6fa2a9c-3218-5a94-b02b-227a2e8eddfe
^
5 = version 5
Generated from: SHA-1 hash of a namespace UUID + a name string. Same inputs always produce the same UUID.
Pros: Deterministic — useful when you need the same ID for the same resource across systems.
Use when: You want a stable ID for a known resource (e.g., uuid5(DNS_NAMESPACE, 'heolab.com') always returns the same UUID).
const { v5: uuidv5 } = require('uuid');
const DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // DNS namespace
const id = uuidv5('heolab.com', DNS);
// Always: 'a6fa2a9c-3218-5a94-b02b-227a2e8eddfe'
01ARZ3NDEKTSV4RRFFQ69G5FAV
ULID (Universally Unique Lexicographically Sortable Identifier) solves UUID's biggest database problem:
const { ulid } = require('ulid');
const id = ulid(); // '01ARZ3NDEKTSV4RRFFQ69G5FAV'
// With custom timestamp
const id = ulid(Date.now());
PostgreSQL, MySQL, and most SQL databases use B-tree indexes. B-trees work best when new records are inserted in order (sequential). UUID v4 is random, causing frequent page splits:
| ID type | Index efficiency | Storage | Sortable |
|---|---|---|---|
| UUID v1 | Good | 16 bytes | Yes |
| UUID v4 | Poor (random) | 16 bytes | No |
| ULID | Excellent | 16 bytes | Yes |
| UUID v7 | Excellent | 16 bytes | Yes |
| SERIAL/BIGINT | Best | 8 bytes | Yes |
Recommendation for PostgreSQL: Use uuid_generate_v7() (Postgres 17+) or ULID for primary keys if you need UUIDs. If you don't need UUID format, BIGSERIAL is still the most performant option.
UUID v7 combines the best of both worlds: timestamp-prefix (like v1) + random suffix (like v4), in standard UUID format:
018e9aee-c65b-7c81-a18c-c2e5e0ac6b28
^
7 = version 7
Natively supported in PostgreSQL 17 and increasingly in UUID libraries. For new projects, v7 is the recommended choice for database primary keys.