UUIDv7 vs ULID
If you’ve been building systems that need unique identifiers, you’ve probably run into the limitations of UUIDv4. Random UUIDs fragment B-tree indexes, cause excessive page splits, and destroy cache locality in databases. Two formats emerged to fix this by putting a timestamp first: ULID (2016) and UUIDv7 (2024, via RFC 9562). They’re remarkably similar in structure but differ in important ways.
The Problem They Both Solve
UUIDv4 generates fully random 128-bit values. When used as a primary key, each insert targets a random location in a B-tree index. Sequential IDs cause 10-20 page splits per million records; UUIDv4 causes 5,000-10,000+. Index pages average ~69% full instead of ~90%, wasting disk space and I/O. Buffer cache effectiveness drops because hot pages are spread across the entire index.
Both UUIDv7 and ULID fix this by putting a millisecond-precision Unix timestamp in the most significant bits. New IDs append to the end of the index, just like auto-incrementing integers, while retaining 128-bit global uniqueness.
Structure
Both formats are 128 bits. Both dedicate the leading 48 bits to a Unix epoch millisecond timestamp. The difference is in how they use the remaining 80 bits.
ULID
01AN4Z07BY 79KA1307SR9X4MV3
|----------| |----------------|
Timestamp Randomness
48 bits 80 bits
All 80 remaining bits are available for randomness. No bits are reserved for format metadata.
UUIDv7
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Six bits are consumed by the version (ver, 4 bits, set to 0111) and variant (var, 2 bits, set to 10) fields.
That leaves 74 bits for randomness: 12 in rand_a and 62 in rand_b. Those 6 bits are the cost of UUID format
compliance.
Encoding
This is where the two formats diverge most visibly.
ULID uses Crockford’s Base32 encoding (5 bits per character), producing a 26-character string:
01ARZ3NDEKTSV4RRFFQ69G5FAV
The alphabet (0123456789ABCDEFGHJKMNPQRSTVWXYZ) omits I, L, O, and U to avoid visual ambiguity. It’s case
insensitive, contains no hyphens or special characters, and is URL-safe.
UUIDv7 uses the standard UUID hex-with-dashes encoding, producing a 36-character string:
01932c08-e690-7584-b945-253de779b977
The 7 after the second dash is the version nibble. This format is instantly recognizable as a UUID by any system that
handles UUIDs.
Monotonicity
Both specs address what happens when multiple IDs are generated within the same millisecond.
ULID increments the random component by 1 in the least significant bit position. If the 80-bit random space overflows within a single millisecond, generation fails. In practice, 2^80 IDs per millisecond is not a realistic concern.
UUIDv7 offers three methods (RFC 9562, Section 6.2):
- Fixed-length counter in
rand_aor the leading bits ofrand_b - Monotonic random: treat the random bits as a counter, increment on each generation
- Sub-millisecond precision: use up to 12 bits of
rand_afor sub-millisecond clock precision
The approach is left to the implementation. PostgreSQL 18’s built-in uuidv7() uses method 3, storing sub-millisecond
precision in rand_a to guarantee monotonicity within a single backend connection.
Standardization
UUIDv7 is defined in RFC 9562, published May 2024 by the IETF. It supersedes RFC 4122 and carries the weight of a formal internet standard. The version and variant bits make it self-describing: any system that understands UUIDs can parse a UUIDv7, detect its version, and extract the timestamp.
ULID is a community specification hosted on GitHub (ulid/spec). It has broad adoption and multiple implementations across languages but is not an IETF or ISO standard. There is no version/variant metadata in the format itself.
Ecosystem Support
UUIDv7 benefits from the UUID ecosystem. Every language, database, and framework already has UUID support. The uuid
column type in PostgreSQL, MySQL’s BINARY(16), and ORMs across every language all handle UUIDs natively. PostgreSQL 18
adds a built-in uuidv7() function. For earlier versions, the pg_uuidv7 extension fills the gap.
ULID requires dedicated libraries. Most languages have mature ULID implementations, but you’ll need to add a dependency
rather than relying on standard library support. Database storage typically means storing ULIDs as CHAR(26),
BINARY(16), or converting to a UUID-compatible binary representation.
Comparison
| ULID | UUIDv7 | |
|---|---|---|
| Size | 128 bits | 128 bits |
| String length | 26 chars | 36 chars |
| Encoding | Crockford Base32 | Hex with dashes |
| Timestamp | 48-bit ms | 48-bit ms |
| Random bits | 80 | 74 (6 used by ver/var) |
| Standardized | Community spec | RFC 9562 |
| UUID-compatible | No | Yes |
| Case sensitive | No | No |
| URL safe | Yes | Needs encoding for dashes |
| Native DB support | Limited | PostgreSQL 18+, growing |
Which One Should You Use?
Use UUIDv7 if you’re working in an ecosystem that already uses UUIDs, need database-native support, or want the backing of a formal standard. In most cases this is the right default. It slots into existing UUID columns, works with existing UUID libraries, and will only gain more native support over time.
Use ULID if you need shorter, more human-readable identifiers, are already using ULIDs in your system, or are working in a context where the 10-character savings matters (URLs, logs, user-facing IDs). The format is also a reasonable choice when you want the full 80 bits of randomness per millisecond.
Either way, both are a significant improvement over UUIDv4 for database primary keys. The timestamp prefix means sequential index inserts, better cache locality, and the ability to extract creation time directly from the ID. If you’re still using UUIDv4 as a primary key, switching to either format is worth it.
One thing to keep in mind: both formats embed a creation timestamp, which means they leak timing information. If that’s a concern (security tokens, API keys, session IDs), UUIDv4 remains the right choice for those specific use cases. A common pattern is UUIDv7 for internal primary keys and UUIDv4 for externally-exposed identifiers.