ULID vs UUID is a choice most teams face when they move away from auto-increment keys. Both are 128-bit identifiers that can be generated anywhere without coordination, but they differ in how they order, how they look in text and how kindly they treat database indexes. This guide compares ULIDs with the UUID versions defined in RFC 9562 (v4, v7 and v1), shows how to decode the timestamp inside a ULID, and ends with practical guidance on which to pick. To inspect an ID as you read, paste it into the ULID decoder.
ULID vs UUID at a glance
| Property | ULID | UUID v4 | UUID v7 |
|---|---|---|---|
| Size | 128 bits | 128 bits | 128 bits |
| Timestamp | 48-bit Unix milliseconds | None | 48-bit Unix milliseconds |
| Random bits | 80 | 122 | 74 (some may be used for a counter) |
| Text form | 26 chars, Crockford Base32 | 36 chars, hex with hyphens | 36 chars, hex with hyphens |
| Sorts by creation time | Yes | No | Yes |
| Standard | Community specification | RFC 9562 | RFC 9562 |
| Native database type | Rarely; usually stored as UUID or binary | Common (uuid) | Common (uuid) |
The table already hints at the main conclusion: a ULID and a UUIDv7 have almost the same structure. The differences are mostly about text encoding and standardisation.
How a ULID is built
The ULID specification defines a 128-bit value made of two parts:
- 48-bit timestamp: milliseconds since the Unix epoch, stored big-endian so it comes first. It will not overflow until the year 10889.
- 80-bit randomness: ideally from a cryptographically secure source, giving 2^80 possible values per millisecond.
The value is written as 26 characters of Crockford Base32, which uses the digits and the uppercase letters except I, L, O and U. The first 10 characters encode the time and the last 16 the randomness. Because 26 characters can hold 130 bits, the first character of a valid ULID is always between 0 and 7.
The encoding was chosen so that string order equals time order: sorting ULIDs as plain text sorts them by creation time, with millisecond precision.
Monotonic mode
Within a single millisecond, random suffixes would sort unpredictably. The spec's optional monotonic mode fixes this: when several IDs are generated in the same millisecond, the generator reuses the previous random part and increments it by one. The ULID generator supports this; four IDs created in one millisecond look like this:
01M3HRR2FG6M6CN1T914K0NCV0
01M3HRR2FG6M6CN1T914K0NCV1
01M3HRR2FG6M6CN1T914K0NCV2
01M3HRR2FG6M6CN1T914K0NCV3
The trade-off is that consecutive IDs become guessable from one another, so monotonic ULIDs should not double as secrets.
The UUID versions worth comparing
RFC 9562, published in 2024, replaced RFC 4122 and added versions 6, 7 and 8. Three versions matter for this comparison:
- UUID v4 is 122 random bits plus fixed version and variant bits. It reveals nothing about when or where it was created, which is its main strength, and it has no useful order.
- UUID v7 puts a 48-bit Unix millisecond timestamp in the first bits, followed by the version, variant and 74 bits that are random or partly used as a counter. It is time-ordered like a ULID, and the RFC recommends it over v1 and v6 for new systems.
- UUID v1 uses a 60-bit count of 100-nanosecond intervals since 1582 plus a clock sequence and a node ID that was historically the MAC address. Its timestamp is stored low bits first, so v1 values do not sort by time, and the node field can leak hardware details.
You can generate v4 and v7 values with the UUID generator and check any UUID's version, variant and embedded time with the UUID validator.
Sortability and database index locality
The biggest practical difference between ULID and UUID v4 shows up in B-tree indexes, which almost every relational database uses for primary keys.
- Random keys (UUID v4) land at random positions in the index. Inserts touch pages all over the tree, which causes more page splits, a larger working set in memory and more write I/O as tables grow.
- Time-ordered keys (ULID, UUID v7) are almost always larger than the previous key, so inserts append near the right edge of the index. Recently written rows sit close together, which also helps queries that read recent data.
Time-ordered keys also give you a free creation timestamp and allow keyset pagination ordered by the ID itself. The cost is that anyone who sees the ID can read when the record was created. If that is sensitive, for example for user account IDs that are shown publicly, UUID v4 is the safer choice.
Storage, text length and URLs
All three are 16 bytes in binary, and that is how they should be stored where possible:
- PostgreSQL has a native
uuidtype, and a ULID fits in it once rewritten as hex. - MySQL and others typically use
BINARY(16). Storing IDs as text works but takes about 26 bytes for a ULID or 36 for a UUID, plus a larger index.
In text form, ULIDs have some advantages. They are 10 characters shorter, contain no hyphens, are case-insensitive and avoid characters that are easy to confuse when read aloud or typed. All three are URL-safe without encoding. UUIDs win on tooling: every language, database and ORM recognises the 8-4-4-4-12 format.
A ULID converted to hex does not carry a meaningful UUID version or variant, because ULIDs do not reserve those bits. Databases store it without complaint, but UUID tools read whatever those random bits happen to spell: usually a non-standard variant or an unknown version, occasionally a real-looking one.
Decoding a ULID timestamp: a worked example
Take the ULID 01J9X2Q4ZC8G7TQ3M5V6W8Y0AB. The first 10 characters, 01J9X2Q4ZC, map to the Base32 values 0, 1, 18, 9, 29, 2, 23, 4, 31 and 12. Reading them as a base-32 number gives 1728626529260 milliseconds. Because 48 bits fit safely inside a JavaScript number, no BigInt is needed:
const B32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
const ulidTime = (ulid) => [...ulid.slice(0, 10).toUpperCase()]
.reduce((ms, ch) => ms * 32 + B32.indexOf(ch), 0);
new Date(ulidTime('01J9X2Q4ZC8G7TQ3M5V6W8Y0AB')).toISOString();
// '2024-10-11T06:02:09.260Z'
The ULID decoder confirms the result and adds the random part and the UUID form:
01J9X2Q4ZC8G7TQ3M5V6W8Y0AB
Timestamp: 2024-10-11T06:02:09.260Z (2 years ago)
Unix ms: 1728626529260
Randomness: 8G7TQ3M5V6W8Y0AB
As UUID: 01927a2b-93ec-440f-ab8e-85d9b88f014b
Because the decoder also accepts UUIDs and both formats begin with the same 48-bit millisecond field, you can paste a UUID v7 and read its time too. The v7 example from RFC 9562 gives:
017f22e2-79b0-7cc3-98c4-dc0c0c07398f
Timestamp: 2022-02-22T19:22:22.000Z (4.6 years ago)
Unix ms: 1645557742000
Randomness: FK1SHH6W1G60EECF
As UUID: 017f22e2-79b0-7cc3-98c4-dc0c0c07398f
The timestamp is correct, but the "Randomness" line here also contains the v7 version and variant bits. For a standard UUID of any other version, such as a random v4, the decoder adds a warning line saying that its first 48 bits are not a timestamp, so the date means nothing. The UUID form of a ULID can look like such a UUID by chance (the one above reads as version 4), so decode ULIDs in their 26-character form. In PostgreSQL, the same timestamp can be read from a uuid column:
SELECT to_timestamp(('x' || substr(replace(id::text, '-', ''), 1, 12))::bit(48)::bigint / 1000.0)
FROM events;
To show the milliseconds in your own time zone, use the Unix timestamp converter.
When to choose ULID and when to choose UUID
Choose UUID v7 when you want time-ordered keys and the widest compatibility. It is an IETF standard, fits native uuid columns with a valid version, and is supported by a growing number of libraries.
Choose a ULID when IDs appear in URLs, logs or support conversations and a shorter, case-insensitive string is worth having, or when your stack already uses ULIDs. Store them as 16 bytes and convert to text at the edges.
Choose UUID v4 when the ID must not reveal creation time, or when insert volume is modest enough that index locality does not matter. For short public tokens rather than primary keys, the Nano ID generator is another option.
Avoid UUID v1 in new designs; v7 provides time ordering without exposing a node identifier.
Summary
ULID and UUID v7 share the same core idea: a millisecond timestamp in front and randomness behind, giving sortable keys that keep B-tree indexes compact. ULID offers a shorter, friendlier text form; UUID v7 offers an official standard and native tooling. UUID v4 remains the right choice when the ID must reveal nothing. If you work with 64-bit integer IDs instead, read the companion guide Snowflake IDs explained.