ULID vs UUID: When Sortable IDs Actually Matter

Glowing abstract data streams arranged in ordered sequences, illustrating the concept of sortable IDs like ULID versus UUID.

The short answer: pick ULID when you need IDs that sort by creation time and stay readable, and pick UUID when you need a battle-tested, universally supported random identifier with no ordering concerns. The core difference in the ulid vs uuid debate is that a ULID embeds a timestamp in its first chunk, so a list of ULIDs naturally sorts in the order they were created, while a classic random UUID (version 4) is deliberately unordered. Both are 128-bit values, so they hold the same amount of uniqueness; they just present and sort it differently.

What ULID and UUID actually are

A UUID (Universally Unique Identifier) is a 128-bit value defined by RFC 4122. The version you see most often is UUIDv4, which is mostly random. It looks like this:

f47ac10b-58cc-4372-a567-0e02b2c3d479

A ULID (Universally Unique Lexicographically Sortable Identifier) is also 128 bits, but it splits those bits into two parts: a 48-bit timestamp (milliseconds since the Unix epoch) followed by 80 bits of randomness. It's defined by the ULID spec and looks like this:

01ARZ3NDEKTSV4RRFFQ69G5FAV

The first 10 characters ( 01ARZ3NDEK ) encode the timestamp. The last 16 are the random part. That single design choice drives almost every practical difference between the two.

Format and encoding side by side

The way each ID is written down matters as much as what's inside it. Here's how they compare:

Property ULID UUID (v4)
Total bits 128 128
String length 26 characters 36 characters (with hyphens)
Encoding Crockford Base32 Hexadecimal
Hyphens None Four
Case sensitive No (uppercase canonical) No
Embedded timestamp Yes No
Lexicographically sortable Yes No

The ULID format uses Crockford Base32, which intentionally skips the letters I , L , O , and U to avoid confusion with 1 , 0 , and each other. That makes a ULID easier to read aloud, type, and double-click to select (no hyphens breaking the selection).

Why ULID is sortable and UUID usually isn't

Because the ULID timestamp sits in the leading bits and Base32 preserves byte order, sorting ULIDs as plain strings produces the same order as sorting by creation time. Generate three ULIDs a few milliseconds apart and they line up like this:

01HQ8M4P3A...   (oldest)
01HQ8M4P9C...
01HQ8M4PF2...   (newest)

A standard UUIDv4 has no such structure. Its bits are random, so sorting a column of UUIDv4 values gives you essentially random order. That randomness is the reason UUIDv4 primary keys can hurt database performance: each new insert lands in a random spot in the B-tree index, scattering writes and fragmenting the index. A monotonic, time-ordered key like a ULID sortable value appends near the end of the index instead, which keeps inserts tidy.

Same instant, still ordered: When multiple ULIDs are generated in the same millisecond, the spec increments the random portion so they stay strictly increasing within that millisecond rather than colliding or shuffling.

When sortable IDs actually matter

Sortability isn't free magic. It helps in specific, concrete situations:

  • Database primary keys at scale: Time-ordered keys reduce index fragmentation and page splits on InnoDB and similar engines, improving insert throughput.
  • Cursor-based pagination: You can paginate by ID alone without a separate created_at column, since the ID already encodes time.
  • Event logs and time-series data: Records naturally appear in chronological order when sorted by primary key.
  • Distributed systems: Each node generates its own ULIDs locally with no coordination, yet the results still sort roughly by time across the whole fleet.
  • Debugging and support: You can eyeball two IDs and tell which came first, which is genuinely useful when reading logs.

If none of those apply, for example you only ever look up rows by exact ID and never sort or range-scan, the sortability buys you very little.

When UUID is still the better choice

UUID has decades of momentum, and that counts for a lot:

  • Native database support: PostgreSQL has a dedicated uuid type; many ORMs, frameworks, and languages ship UUID generators in the standard library. ULID often needs a third-party package.
  • You don't want to leak timing: A ULID reveals exactly when it was created. For public-facing IDs that could expose business metrics (like guessing daily signup volume), that's a downside.
  • Strict interoperability: If an external API, payment processor, or partner system expects RFC 4122 UUID strings, sending a 26-character Base32 ULID will break validation.
  • Maximum unpredictability: UUIDv4 dedicates 122 bits to randomness versus ULID's 80, so it's slightly harder to guess (though 80 bits is still astronomically large).
Neither ID type is a security token. Both can appear in URLs and logs. Don't rely on either being "unguessable" to protect sensitive resources; use real authorization checks.

UUIDv7 and other alternatives

The line between the two has blurred. The newer RFC 9562 introduced UUIDv7, which puts a Unix millisecond timestamp in the high bits, just like ULID. So UUIDv7 is both time-sortable and a valid RFC UUID, giving you ordering without abandoning the standard hex format and existing tooling.

Among the common uuid alternatives:

  • UUIDv7: Sortable, standard-compliant, and the best of both worlds if your stack supports it.
  • ULID: Shorter string, friendlier encoding, but not an RFC 4122 UUID.
  • Snowflake IDs: 64-bit, time-ordered, used by Twitter and Discord, but require coordinated machine IDs.
  • KSUID: Similar idea to ULID with a second-resolution timestamp and a longer random component.

If you're starting fresh today and your database and language already support it, UUIDv7 is often the easiest recommendation. Choose ULID when you specifically want the compact Base32 string and don't need RFC compliance.

How to pick for your project

Run through these questions in order:

  1. Do external systems require RFC UUIDs? If yes, use a UUID (and prefer v7 if you also want ordering).
  2. Do you sort, paginate, or range-scan by ID? If yes, you want a time-ordered ID: ULID or UUIDv7.
  3. Would exposing creation time leak sensitive info? If yes, lean toward UUIDv4 for public IDs.
  4. Do you value a shorter, human-friendly string? ULID's 26 characters beat UUID's 36.
  5. Is broad library and database support critical with zero extra dependencies? UUID wins on ubiquity.

For most internal, database-backed applications where you control both ends, a time-sortable ID (ULID or UUIDv7) is the better default. For public APIs and cross-vendor integrations, plain UUID remains the safe, expected choice.

Generating sortable ULID and UUID identifiers online

Generate ULIDs and UUIDs without writing code

Need a quick ULID or UUID to test your sortable IDs setup? Generate valid identifiers in seconds, copy them, and drop them straight into your database or API while you decide between ulid vs uuid.

Generate an ID now →

Not quite. Both are 128-bit identifiers, but a ULID uses Crockford Base32 encoding and is not a valid RFC 4122 UUID. If you want a sortable identifier that is also a real UUID, use UUIDv7 instead, which embeds a timestamp while keeping the standard hex format.

Yes, because a ULID is 128 bits, you can store its raw binary form in any 16-byte UUID column. The string representation differs, but the underlying bytes fit perfectly. Many teams store ULIDs as binary(16) or in a native UUID type and convert to the Base32 string only when displaying.

It can. Anyone holding a ULID can decode its millisecond creation time, which might reveal signup patterns, order volumes, or other timing data. For public-facing identifiers where that matters, prefer a fully random UUIDv4 so no creation time leaks out of the ID itself.

Within a single millisecond you have 80 random bits, which is roughly 1.2 x 10^24 possible values. Generating millions of ULIDs per millisecond would still leave collision odds vanishingly small. Across different milliseconds, the timestamp itself adds separation, so collisions are not a practical concern.

Usually not worth it for an existing system. Migrating primary keys is risky and touches foreign keys, caches, and external references. Switch only if you have a measured insert-performance problem from random UUIDs. For new tables, choosing a time-ordered ID like ULID or UUIDv7 from the start is the easier path.