What they have in common

UUID v7 and ULID solve the same core problem: a globally unique identifier that is also sortable by creation time, generated without a central authority. Both are 128 bits. Both embed a 48-bit millisecond Unix timestamp in the high bits. Both have ~80 bits of randomness in the low bits. If you stored the raw bytes of each, they would look nearly identical.

The differences are in encoding, monotonicity guarantees, and ecosystem support.

Encoding: hyphenated hex vs Base32

A UUID v7 uses the standard hexadecimal format with hyphens:

018f3c4e-7a21-7b3c-9d4e-5f6a7b8c9d0e   (36 characters)

A ULID uses Crockford Base32 (no hyphens, uppercase, no ambiguous characters):

01HQ5Z6K4TQAJ3BMKYBVP1R2GF             (26 characters)

The ULID string is 28% shorter. Both encode the same 128 bits. For systems where identifier strings appear in URLs, log lines, or user-facing interfaces, the shorter format is a visible advantage. For systems where the identifier is stored in a database column and never exposed directly, the length difference is irrelevant.

Monotonicity

Both formats deal with the case where multiple IDs are generated within the same millisecond.

UUID v7 with RFC 9562 §6.2 Method 1 uses a 12-bit per-millisecond counter in the rand_a field. Values generated in the same millisecond increment the counter, ensuring sort order matches generation order. The counter resets each millisecond and has a ceiling of 4,096 values per millisecond per generator instance.

ULID uses the full 80 random bits for monotonicity: the first ULID in a millisecond gets random low bits, and each subsequent ULID in the same millisecond increments those bits by 1. This means the random portion decreases by one for every generated value — the available entropy per millisecond is 2⁸⁰ rather than 2⁷⁴ (the UUID v7 random portion after the counter).

In practice, both approaches are sufficient for any realistic generation rate. The ULID approach has a slightly larger monotonic space; the UUID v7 approach has a cleaner separation between the counter and random bits.

Database support

This is where UUID v7 has a clear advantage.

DatabaseUUID v7ULID
PostgreSQLNative uuid type, uuidv7() in PG18No native type — store as CHAR(26) or decode to uuid
MySQLBINARY(16) with UUID functionsBINARY(16) with manual encoding
SQL Serveruniqueidentifier (byte order caveat)No native type
SQLiteBLOB or TEXTTEXT(26)
MongoDBBinary subtype 4No native type

PostgreSQL’s native uuid type stores 16 bytes and does bitwise comparison. Storing a ULID in PostgreSQL requires either a CHAR(26) column (26 bytes, string comparison) or decoding the Crockford Base32 back to bytes and storing as uuid — which works but adds a conversion step everywhere.

For most database-backed applications, UUID v7 is the lower-friction choice because native type support handles storage, indexing, and comparison without any extra encoding logic.

Ecosystem and library support

UUID v7 is part of an IETF standard (RFC 9562, published 2024). Support is built into:

ULID is a community specification with no IETF backing. Libraries exist for most languages (ulid on npm, python-ulid, github.com/oklog/ulid for Go) but there is no native database or language runtime support. You always need an external dependency.

When to choose ULID

ULID makes sense when:

When to choose UUID v7

UUID v7 makes sense when:

Side-by-side comparison

PropertyUUID v7ULID
StandardRFC 9562 (IETF, 2024)Community spec
String length36 chars (with hyphens)26 chars
EncodingHexadecimalCrockford Base32
Timestamp precision48-bit millisecond48-bit millisecond
Random bits74 (with 12-bit counter)80 (monotonic increment)
Native DB type (PostgreSQL)Yes — uuidNo
Native language supportPython 3.13, .NET 9No
SortableYesYes
URL-safeYes (hyphens are safe)Yes (no hyphens)

For new projects using a relational database, UUID v7 is the safer default. For systems where IDs are primarily strings in logs or APIs and database native types are not a concern, ULID is a reasonable alternative.

To start generating UUIDs now, use the UUID v7 generator. For the full UUID v4 vs v7 comparison, see UUID v4 vs UUID v7.

Frequently asked questions

What is a ULID?

ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit identifier that encodes a 48-bit millisecond timestamp followed by 80 bits of randomness, represented as a 26-character Base32 string. It is time-sortable and URL-safe but has no IETF standard and no native database type support.

Should I use ULID or UUID v7?

UUID v7 is the safer default for database-backed applications — it has RFC standardisation, native PostgreSQL uuid type support, and growing ORM integration. ULID produces shorter 26-character strings and is a reasonable choice when IDs are primarily used as strings in logs or APIs and database native types are not a concern.

Should I use v4 or v7?

Use v7 for database primary keys (time-sortable, index-friendly) and v4 for anything where creation order could leak information, like tokens or share links.

What is a ULID?

ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit identifier that encodes a 48-bit millisecond timestamp followed by 80 bits of randomness, represented as a 26-character Base32 string. It is designed to be time-sortable and URL-safe without hyphens.

Is ULID better than UUID v7?

Neither is strictly better. UUID v7 has RFC standardisation and native database type support. ULID produces shorter strings and guarantees strict per-millisecond monotonicity by default. The right choice depends on whether standard compatibility or string compactness matters more to your project.

Can I use ULID in PostgreSQL?

Yes — store it as uuid (if you treat the bits as a UUID) or as CHAR(26). PostgreSQL has no native ULID type, so you lose native uuid column benefits unless you encode the ULID bytes as a UUID. UUID v7 is generally simpler in PostgreSQL because the native uuid type handles it directly.

Can I mix UUIDs and ULIDs in the same column?

Not directly — they use different string formats (hyphenated hex vs Base32). You can store both as raw bytes in a BINARY(16) or uuid column, but querying requires consistent encoding. Mixing formats in the same column is not recommended.