Random UUID v4 has been the default choice for fifteen years, and in modern databases it turns out to be an expensive decision: identifiers arrive in completely random order, fragmenting B-tree indexes and wrecking the page cache. UUIDv7 — officially standardized in RFC 9562 (2024) — and ULID attack that problem from slightly different angles. Here's the comparison you're missing before choosing a primary key.
The v4 problem: inserts at random
A B-tree keeps data ordered by key. When every new row carries an unpredictable ID, its insertion position is random:
- The destination page may not be in memory → disk read.
- It may be full → page split.
- The buffer pool fills with scattered hot pages used only once.
In PostgreSQL this has a known name: index bloat and write amplification. With millions of daily rows, the difference versus sequential IDs is measurable in orders of magnitude of writes. Auto-incrementals avoided the problem... at the cost of revealing business volume in URLs (/orders/48392 says how many orders you have) and requiring central coordination to assign IDs.
UUIDv7: timestamp + randomness
Version 7 packs a 48-bit Unix millisecond timestamp followed by 74 random bits:
018f 6a1c b2d4 7xxx yxxx xxxxxxxxxxxx
└── timestamp ms ──┘└─ rand_a + rand_b ─┘
Resulting properties:
- Chronologically sortable: two v7s generated at different moments sort the same as strings and as bytes.
ORDER BY id≈ORDER BY created_atwithout an extra column. - Nearly sequential insertion: new records always land "at the end" of the index. Goodbye random splits.
- Unique without coordination: random bits guarantee uniqueness even across machines generating thousands per millisecond.
- Free date: decoding the first bytes reveals when the record was born.
Native generation already available:
crypto.randomUUID(); // always v4 (for now)
// v7 via library or manual implementation:
function uuidv7() {
const ts = Date.now();
const bytes = crypto.getRandomValues(new Uint8Array(10));
const hex = [
ts.toString(16).padStart(12, "0").slice(0, 12),
[...bytes].map(b => b.toString(16).padStart(2, "0")).join("")
].join("");
// sets version 7 and variant per RFC 9562
return `${hex.slice(0,8)}-${hex.slice(8,12)}-7${hex.slice(13,16)}-a${hex.slice(17,20)}-${hex.slice(20,32)}`;
}
PostgreSQL 18 ships native uuidv7(); extensions (pg_uuidv7) and client libraries cover earlier versions.
ULID: the cousin with Crockford Base32
ULID (Universally Unique Lexicographically Sortable Identifier) solved the same problem in 2016 with two design differences:
- 128 bits all the same, but encoded in 26-character Crockford Base32:
0123456789ABCDEFGHJKMNPQRSTVWXYZ(no I, L, O, U to avoid visual confusion). - 48 bits of timestamp + 80 random, no dashes or visible internal structure:
01ARZ3NDEKTSV4RRFFQ69G5FAV
Practical advantages: 26 characters versus 36, string-sortable in any system without parsing, shorter in URLs. Disadvantage: not an ISO/RFC standard, so ecosystems outside JS/Go/Java need a library, while UUIDv7 is now an international norm with growing runtime and database support.
Direct comparison
| Property | UUID v4 | UUID v7 | ULID |
|---|---|---|---|
| Randomness | 122 bits | 74 bits | 80 bits |
| Temporal order | No | Yes (ms) | Yes (ms) |
| Text size | 36 chars | 36 chars | 26 chars |
| Standard | RFC 9562 | RFC 9562 | Own spec |
| Embedded date | No | Yes | Yes |
| Native DB support | Universal | Growing | Via extension |
Note on the lost bits: v7 keeps statistical uniqueness with room to spare for any real workload; a collision requires matching millisecond AND matching 74 random bits across concurrent generators. Not a practical concern.
When to stick with v4
Not everything is migration: opaque session tokens, exposed API keys, identifiers where filtering by date would leak sensitive info — there, v4's total absence of structure is precisely the virtue. A v7 in public URLs donates its creation timestamp.
And one applied security warning: if you use sortable IDs on public endpoints, pair them with correct authorization — ordering doesn't replace permissions, it just makes neighbors predictable (/invoices/018f6a1c... invites trying the next one).
Generate and verify identifiers
Our UUID generator produces v4 and variants instantly for testing, and if you need a refresher on how v4 works internally with crypto.randomUUID(), see the complete UUID guide in JavaScript.
FAQ
Can I migrate an existing table from v4 to v7? You can use v7 for NEW records without touching old ones: the index accepts both. Reordering history rarely pays off.
Does UUIDv7 leak information? Yes, deliberately: it exposes creation date. If that's a problem in your domain, stay on v4.
What about Twitter's Snowflake IDs? Same idea (timestamp + worker + sequence) but they require worker-ID coordination across machines. v7/ULID achieve the essentials without that infrastructure.
Generate unique identifiers instantly with our online UUID generator, free and right in your browser.