← Back to Blog

ULID vs UUID: A Practical Guide to Time-Sortable Identifiers

Start with the classic key-selection dilemma

When picking a primary key for a message or order table you will eventually face this choice:

UUID v4 : 550e8400-e29b-41d4-a716-446655440000   (random, 36 characters)
ULID    : 01HZ3G7DZP5XA5Z4G5TQXW2Q7T            (sortable, 26 characters)

Both are globally unique and can be generated offline, but one sorting test exposes the split: order 100 rows by UUID and then shuffle them by creation time—the sort order and real order are unrelated. Switch to ULID and lexicographic order equals creation order.

That single difference drives how they behave in database indexes, log aggregation and debuggability.

How ULID encodes time into its prefix

ULID is just 48 bits of timestamp + 80 bits of randomness:

 48 bit     80 bit
 timestamp  randomness
 ────────── ───────────────────────────── ──
 01HZ3G7DZP              5XA5Z4G5TQXW2Q7T
  • The first 48 bits are milliseconds since the Unix epoch, encoded as 10 Base32 characters;
  • The remaining 80 bits are cryptographically random bytes, encoded as 16 characters;
  • Total is 26 characters from the Crockford Base32 alphabet (excluding I/L/O/U).

To decode the timestamp, reverse-encode the first 10 characters. This is the root of sortability—compare time first, and only the randomness when times are equal.

Comparing the three mainstream identifier families

Property UUID v4 ULID Snowflake-like
Length 36 chars 26 chars ~19 decimal digits
Time-sortable No Yes (ms) Yes (second + sequence)
Offline generation Fully offline Fully offline Needs node/coordinator
Strict cross-node order No No (clock-drift bound) Implementation-dependent
Readability Medium (dashed groups) High (compact) High (plain digits)

The Snowflake family trades offline generation (it needs a worker ID) to be shorter or to align with a single datacenter; ULID generates offline as a pure function in most languages, making it the simplest to deploy.

Keeping it monotonic within a millisecond

Many implementations add a monotonic mode: when generated again in the same millisecond, the random bits act as an incrementing counter until they overflow or the next millisecond arrives. This matters for sequential inserts—out-of-order rows in the same millisecond would still cause page splits.

// JavaScript sketch: increment the random bits within the same millisecond
let lastGen = null, lastRand = 0n
function ulid() {
  const now = Date.now()
  if (now === lastGen) lastRand++          // monotonic
  else { lastGen = now; lastRand = 0n }
  return encode(now, lastRand)
}

Why ordered primary keys matter to databases

Cloudy-indexed engines (like InnoDB) treat the primary key as physical order. A random key inserts a new row in the middle of a page, causing page splits, random IO and cache misses; an ordered key always appends at the page tail. At tens of millions of rows the difference in write throughput and cache hit ratio becomes dramatic.

That is why, when you generate a key with a ULID generator, you are usually after "globally unique plus roughly chronological"—often a better database-friendly default than UUID v4.

One trade-off to see clearly: privacy

Because ULID embeds its timestamp, anyone who holds the ID knows when it was created. That is both a debugging lifesaver and a possible leak toward the user. Good practice:

  • For user-facing public identifiers (order IDs, invite codes) where you must hide time, prefer UUID v4;
  • For server-internal keys, database PKs, logs and event IDs, prefer ULID;
  • For compliance-sensitive creation time, store the time field encrypted separately rather than leaking it through a sortable ID.

Suggested selection rule

Choose ULID when: you want ordered writes to a relational PK, logs that aggregate by time, simple offline generation, and a shorter value.

Choose UUID v4 when: the value is exposed to end users, you strictly don't want to leak time, or you need standard interop with external systems.

Choose Snowflake/DB sequences when: you hard-need strict global ordering across nodes and can supply a worker ID across datacenters.

Try it yourself

Generate a batch of ULIDs, sort them lexicographically and compare with the encoding—they will line up in creation order. Re-run the same with UUID v4 and the order will scramble. That experiment makes the properties stick.

Frequently Asked Questions

What is the core difference between ULID and UUID v4?

The key difference is time-sortability: ULID encodes a millisecond timestamp into its first 48 bits, so lexicographic order equals creation order; UUID v4 is fully random, so creation order has nothing to do with sort order. ULID is also more compact—26 characters (Crockford Base32) vs 36 for UUID v4. The trade-off: ULID leaks its creation time (a privacy consideration) and can be sorted or correlated by timestamp.

Why do distributed systems prefer ULID over UUID?

Relational databases prefer sequential keys: B+ tree inserts stay ordered, reducing page splits, random IO and cache misses. ULID provides near-time order and can be made monotonic within the same millisecond, avoiding the index fragmentation and poor cache locality that random UUID v4 keys cause on huge tables. ULID keeps global uniqueness while adding chronology—ideal for logs, event sourcing and message queues that naturally aggregate by time.

Does ULID leak information?

Yes—and it is an intentional trade-off. The 48-bit timestamp prefix can be decoded back to a millisecond creation time, so anyone holding a ULID can infer when the ID was created. For public identifiers exposed to end users (order IDs, coupon codes) that may be undesirable; for internal database keys and logs the traceability is a feature. Crockford Base32 deliberately excludes confusable characters (I/L/O/U) to cut manual copy errors.

Can ULID guarantee ordering without a unified clock across nodes?

Not strictly. ULID relies on each node's local clock; clocks across time zones or with drift break global ordering. Common mitigations: enable per-node monotonic generation, align with NTP, and accept a bounded skew window. If your business strictly requires globally-strict cross-node ordering, use a distributed-ordering scheme (Snowflake-family, DB sequences, or a coordinator) instead of plain ULID.

← Back to Blog