Why Unique ID Format Matters
Unique identifiers are everywhere — database primary keys, API IDs, request tracking, distributed systems. Choosing the wrong format is invisible at small scale but causes real problems at millions of rows: slower inserts, index fragmentation, and inability to sort by creation time.
UUID v4, UUID v7, and ULID are all 128-bit identifiers — but they differ fundamentally in how those bits are structured.
UUID v4: Pure Randomness
Example: 550e8400-e29b-41d4-a716-446655440000
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
←—————————— 122 random bits ——————————→UUID v4 uses 122 bits of cryptographic randomness. It has no structure — you cannot tell when it was created or sort a list by generation order.
The database problem: B-tree indexes expect new values to be larger than old ones (like auto-increment). UUID v4 inserts at a random position in the B-tree, causing page splits and cache misses. At millions of rows, this degrades INSERT performance by 2–10×.
UUID v7: Timestamp + Randomness (RFC 9562)
Example: 018f0c5a-2b4e-7000-8123-456789abcdef
←— 48-bit ms timestamp —→ ←— random —→
018f0c5a-2b4e = milliseconds since Unix epoch
7 = version 7
000-8123... = random bitsUUID v7 is standardized in RFC 9562 (2024). Its 48-bit millisecond timestamp prefix means every new UUID is greater than all previous ones — new rows always insert at the end of the B-tree index, just like auto-increment integers.
Database support: PostgreSQL 17+ has a native uuidv7() function. For earlier versions, use the uuid npm package (v9+) or the pg-ulid extension.
ULID: Lexicographically Sortable
Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV
←——— 10 chars ———→ ←—— 16 chars ——→
48-bit timestamp 80-bit randomness
(Base32 Crockford encoding)ULID uses the same timestamp + random structure as UUID v7 but encodes as a 26-character Base32 string. It is shorter, URL-safe, case-insensitive, and guarantees strict monotonic ordering within the same millisecond.
ULIDs are not part of any IETF standard and require a library in every language/database. They cannot be stored in native UUID database columns without conversion.
Head-to-Head Comparison
| Property | UUID v4 | UUID v7 | ULID |
|---|---|---|---|
| Format | 8-4-4-4-12 hex | 8-4-4-4-12 hex | 26-char Base32 |
| Length (string) | 36 chars | 36 chars | 26 chars |
| Sortable | No | Yes (ms precision) | Yes (ms + monotonic) |
| Timestamp encoded | No | Yes | Yes |
| DB insert performance | Poor at scale | Excellent | Excellent |
| Native DB UUID type | Yes | PostgreSQL 17+ | No (store as text/bytes) |
| Reveals creation time | No | Yes | Yes |
| Standard | RFC 4122 / 9562 | RFC 9562 (2024) | spec.ulid.io (unofficial) |
| Library needed | No (built-in) | Yes for older DBs | Yes (always) |
| URL-safe | No (hyphens) | No (hyphens) | Yes |
Database Performance: The Real Numbers
The difference becomes measurable around 1–5 million rows and significant at 10M+. A typical benchmark on PostgreSQL inserting 10 million rows:
| ID Type | Inserts/second | Index size | Page splits |
|---|---|---|---|
| BIGSERIAL | ~85,000 | Baseline | None |
| UUID v7 / ULID | ~75,000 | ~1.1× | Minimal |
| UUID v4 | ~25,000 | ~1.6× | Frequent |
Approximate figures — actual numbers vary by hardware, PostgreSQL version, and workload.
Decision Guide: Which to Use?
- Use UUID v4 when you need to hide creation time (public API IDs exposed to users), for request IDs / nonces where randomness matters, or for non-database identifiers. UUID v4 leaks nothing — UUID v7 and ULID embed a timestamp.
- Use UUID v7 for database primary keys on PostgreSQL 17+ or when you need RFC-standard sortable UUIDs with good ecosystem compatibility. It fits into existing UUID columns and tooling.
- Use ULID if you prefer the shorter Base32 format for readability/URLs, need strict monotonic ordering within the same millisecond, or are working in an environment without native UUID v7 support.
Generating Each Format
// JavaScript / Node.js
import { v4, v7 } from 'uuid'; // npm install uuid
import { ulid } from 'ulid'; // npm install ulid
const uuid4 = v4(); // "550e8400-e29b-41d4-a716-446655440000"
const uuid7 = v7(); // "018f0c5a-2b4e-7000-8123-456789abcdef"
const id = ulid(); // "01ARZ3NDEKTSV4RRFFQ69G5FAV"
// PostgreSQL 17+
SELECT uuidv7(); -- native UUID v7
-- PostgreSQL < 17 (using pg extension)
SELECT gen_random_uuid(); -- UUID v4 only; add extension for v7
// Python
import uuid
uuid4 = str(uuid.uuid4()) # "550e8400-..."
uuid7 = str(uuid.uuid7()) # Python 3.14+ stdlib
# or: pip install uuid7Key Takeaways
- UUID v4 is unordered — bad for DB primary keys at scale, good when you need to hide creation time
- UUID v7 adds a timestamp prefix — dramatically better INSERT performance, sorts chronologically
- ULID is UUID v7 in Base32 — shorter string, strictly monotonic, not an IETF standard
- For new projects with PostgreSQL 17+, UUID v7 is the default choice for primary keys
- For public-facing IDs, keep using UUID v4 to avoid leaking creation time