Skip to content
Back to Blog
Tutorials

What Is a ULID? Sortable Unique Identifier Guide

What is a ULID? How the sortable 128-bit ID works: its timestamp-plus-randomness structure, Crockford Base32 encoding, and when to pick it over a UUID.

12 min read

What Is a ULID? The Sortable Unique Identifier, Explained

Every random UUIDv4 you insert as a primary key lands at an unpredictable spot in the database index. Do that a few million times and the index fragments, the cache thrashes, and writes slow down. A ULID fixes that without giving up what you liked about UUIDs: you can still mint one anywhere, with no central coordinator, but it lands in time order instead of scattering.

So how does a 26-character string sort itself by time? That is the whole trick, and it is worth understanding before you reach for one.

A ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit identifier written as 26 Crockford Base32 characters. The first 10 characters encode a millisecond timestamp and the last 16 encode random bits, so ULIDs created later always sort after earlier ones when compared as plain strings. It is a sortable unique identifier you can generate offline.

This guide takes that apart: the anatomy decoded character by character, the proof it really sorts, the B-tree math behind the database win, and an honest look at what the embedded timestamp leaks. You can follow along with a live value in the ULID generator — generate one, decode it, convert it to a UUID — while you read.

What Is a ULID?

A ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit identifier designed as a more sortable, more compact alternative to a UUID. It is written as 26 characters of Crockford Base32: the first 10 hold a 48-bit timestamp in milliseconds since the Unix epoch, and the remaining 16 hold 80 bits of randomness. Because the time comes first, the string sorts chronologically.

That last property is the reason the format exists. UUIDv4 is fully random, which is great for uniqueness but means two IDs created a second apart have no relationship to each other. ULIDs keep the coordination-free, generate-anywhere model and add time-ordering on top, so a column of them is naturally sorted by creation time with nothing extra.

Here is the format at a glance:

PropertyValue
Bits128
Encoding26 Crockford Base32 characters
Layout48-bit timestamp + 80-bit randomness

The rest of this article fills in how each piece works. The encoding and the sortability each get their own section below. First, the layout.

Anatomy of a ULID: 48 Bits of Time + 80 Bits of Randomness

A ULID’s 26 characters split cleanly into two halves. The first 10 characters are the timestamp; the last 16 are the random part. Lay the canonical example out and the boundary is obvious:

01ARYZ6S41   TSV4RRFFQ69G5FAV
└────────┘   └──────────────┘
 10 chars        16 chars
48-bit ms      80-bit random
timestamp

Two components, two jobs. One records when the ULID was created; the other guarantees uniqueness. Each is decoded below.

The 48-bit timestamp (first 10 characters)

The leading 10 characters encode a 48-bit integer: the number of milliseconds since the Unix epoch at the moment the ULID was created. Take the canonical example straight from the spec:

01ARYZ6S41  ->  1469918176385 ms  ->  2016-07-30T22:36:16.385Z

That is a real, reversible decode — paste 01ARYZ6S41TSV4RRFFQ69G5FAV into a decoder and you get exactly 2016-07-30T22:36:16.385Z back. The time component is plain data, not a hash, so reading it costs nothing.

One small detail that trips people up: the first character of a ULID is always between 0 and 7. A Crockford character holds 5 bits, and 48 bits is not a multiple of 5. The timestamp occupies the low 48 of the 50 bits that 10 characters can carry, which leaves the top 2 bits of the first character permanently zero. Two zero bits cap that character’s value at 7. If you ever see a ULID starting with 8 or higher, it is malformed.

The 80 bits of randomness (last 16 characters)

The remaining 16 characters carry 80 bits of randomness, and this half is where uniqueness comes from. The bits should come from a cryptographically secure source — crypto.getRandomValues in the browser, not Math.random. The difference matters: Math.random is predictable enough that an attacker could guess or collide values, while a CSPRNG is not.

How much room is 80 bits? Roughly 1.2 × 10²⁴ possible values, and that is per millisecond. Even if you mint millions of ULIDs inside a single millisecond, the odds that two draw the same 80 bits stay vanishingly small. Unlike the timestamp, this half carries no decodable meaning — it is noise whose only purpose is to make every ULID distinct.

Crockford’s Base32: Why ULIDs Drop I, L, O, and U

ULIDs are encoded with Crockford’s Base32, an alphabet of 32 symbols: the digits 09 and the letters AZ with four removed.

0123456789ABCDEFGHJKMNPQRSTVWXYZ

The missing letters are I, L, O, and U. Three are dropped because they look like digits — I and L resemble 1, O resembles 0 — so a human reading a ULID off a screen can’t confuse a letter for a number. The flip side is forgiving input: a compliant decoder maps I and L back to 1 and O to 0, and treats the whole string case-insensitively. U is excluded separately, to avoid accidentally spelling out offensive words.

The bit math is the other reason. Each Base32 character encodes 5 bits, where a hexadecimal character encodes only 4. Pack 128 bits at 5 bits per character and you need 26; pack the same 128 bits at 4 bits each — the way a UUID does — and you need 32, plus four hyphens, for 36 characters. So a ULID is meaningfully shorter than a UUID and, with no hyphens, drops straight into a URL, a filename, or a header without escaping.

Crockford’s Base32 is an alphabet of 32 symbols (09 and AZ minus I, L, O, U) that encodes 5 bits per character. ULIDs use it to pack 128 bits into 26 case-insensitive, URL-safe characters, and — crucially — the alphabet is in ascending order, which is what lets the encoded string sort the same way as the raw bits.

Why ULIDs Sort by Time

Lots of articles tell you ULIDs sort by time. Fewer show why. The reason rests on two facts you already have: the timestamp is the most significant part of the value, and Crockford’s alphabet is laid out in ascending order.

Put those together and you get a chain of equivalences:

string compare  ==  128-bit integer compare  ==  creation-time compare

Comparing two ULIDs character by character (the way a string sort works) gives the same answer as comparing their underlying 128-bit integers, because the alphabet is order-preserving: a “higher” character always means a higher value. Comparing the 128-bit integers gives the same answer as comparing creation times, because the timestamp sits in the most significant bits, so it dominates the comparison; the random tail only breaks ties within the same millisecond. String order, bit order, and time order are the same order.

A quick demonstration. Two ULIDs minted one millisecond apart:

01ARYZ6S41...   (created at T)
01ARYZ6S42...   (created at T + 1 ms)

The tenth character ticks from 1 to 2, and a plain text sort puts the second after the first — no timestamp column, no special comparator. The practical payoff, which the next section expands, is one line: ORDER BY id returns rows in chronological order with no extra index.

ULIDs as Database Primary Keys: B-Tree Locality

Most relational databases store a primary-key index as a B-tree, and where a new key lands in that tree decides how expensive the insert is. This is where ULIDs earn their keep.

A random UUIDv4 lands somewhere unpredictable on every insert:

UUIDv4: each new key targets a random leaf page. The page is often full, so the engine splits it, copies half the rows elsewhere, and dirties pages all over the tree. Across millions of rows this fragments the index, evicts useful pages from the buffer cache, and drags down insert throughput. (For the hard index page-split numbers — typically a 2–10× difference on write-heavy tables — see the comparison guide.)

A time-prefixed ULID lands at the end every time:

ULID: because the high bits are a timestamp, each new key is greater than the last, so it appends at or near the right edge of the index. Inserts stay sequential, page splits nearly disappear, the index stays compact, and a range scan over a time window reads a contiguous run of pages.

You get the coordination-free generation of a UUID with the insert locality of an auto-increment integer — without exposing a guessable sequential counter, since the random tail still hides the exact next value.

Storage tip: store the 128 bits as 16 binary bytes — a uuid column in PostgreSQL, BINARY(16) in MySQL — not as a 26-character text field, which wastes space and bloats the index. Encode to the Base32 string only at the edges where a human or a URL sees it. The generator’s Convert tab will convert a ULID to a UUID for exactly this, since the two forms are the same 128 bits.

Monotonic ULIDs: Strict Order Within a Millisecond

The sortability proof has one honest gap: within a single millisecond, plain ULIDs are not strictly ordered. They share the same 10-character time prefix, but their 80-bit random tails are drawn independently, so which of two same-millisecond ULIDs sorts first is essentially a coin flip. For most uses that is fine. When you need strict order even at sub-millisecond rates, it is not.

Monotonic generation closes the gap. The rule is simple: the first ULID in a given millisecond gets fresh randomness as usual, and every later ULID in that same millisecond is produced by taking the previous 80-bit random value and incrementing it by one (treated as a big-endian integer, carrying into higher bits as needed). Each value is therefore strictly greater than the one before it.

You can see it in a batch generated inside one millisecond — only the final character moves:

01KVT0F720ZK9N4T2QX7VR8WMC
01KVT0F720ZK9N4T2QX7VR8WMD
01KVT0F720ZK9N4T2QX7VR8WME

…WMC < …WMD < …WME, guaranteed. This matters whenever rows can be created faster than the millisecond clock ticks: high-throughput inserts, event logs, message IDs in a tight loop. When the clock advances to the next millisecond, generation reverts to fresh randomness and the cycle repeats.

ULID vs UUID: When to Use Which

The question most people actually arrive with is ULID vs UUID. Here is the focused comparison — ULID against the two UUID versions you’d realistically weigh it against. (For the full five-way decision matrix including Snowflake and NanoID, see the full comparison of ULID, UUID and Snowflake.)

PropertyULIDUUIDv4UUIDv7
Length26 chars36 chars36 chars
EncodingCrockford Base32Hyphenated hexHyphenated hex
Sortable by time?YesNoYes
Embeds timestamp?Yes (48-bit ms)NoYes (48-bit ms)
Standardized?Community specRFC 9562RFC 9562
Best forShort sortable IDsOpaque random IDsSortable IDs in UUID format

In prose: reach for a ULID when you want the shortest, URL-safe, sortable string. Reach for UUIDv4 when you want an opaque, fully random identifier with no embedded time — for example a public token where you’d rather not reveal when it was created. Reach for UUIDv7 when you need time-ordering but must stay inside the standard UUID format, with version and variant bits in their fixed positions and a native uuid column to drop it into.

All three are 128 bits, so ULID ↔ UUID conversion is lossless either way. The relationship between ULID and ulid vs uuid v7 is closer than it looks: UUIDv7 is essentially the IETF-standardized take on the same time-prefixed idea ULID pioneered. If you’re new to UUIDs altogether, start with the fundamentals first, then come back to this comparison.

The Privacy Trade-Off: ULIDs Leak Their Creation Time

The embedded timestamp is a feature and a leak, depending on who reads the ID. Anyone holding a ULID can decode the timestamp in one step and learn the exact millisecond the record was created — no access to your database required.

Inside your own systems that is pure upside: instant auditing, free ordering, easy debugging. On a public-facing identifier it is a real disclosure. The creation time can be business-sensitive on its own, and a handful of ULIDs sampled over time leak your creation rate: how many orders, accounts, or messages you mint per second. That is the kind of thing competitors and scrapers like to estimate.

To be fair, this is a narrower leak than UUIDv1, which historically embedded the generating machine’s MAC address; a ULID exposes only time, never hardware identity. Still, weigh it. The simple mitigation: keep ULIDs internal and hand out a fully random UUIDv4 for public-facing IDs where ordering doesn’t matter.

Common Pitfalls with ULIDs

Most ULID trouble is a handful of avoidable engineering decisions, not bugs in the format. The recurring ones:

  • Assuming same-millisecond plain ULIDs are ordered. They share a time prefix but have independent random tails, so their order is undefined. Fix: use monotonic mode when you need strict ordering at sub-millisecond rates.
  • Storing a ULID as 26-char text. That wastes space and inflates the index. Fix: store the 128 bits as 16 bytes (uuid / BINARY(16)) and encode to Base32 only at the edges.
  • Expecting a ULID→UUID conversion to report as v4 or v7. Conversion re-encodes the same bits; it does not set the UUID version and variant fields, so a library inspecting them won’t see a tagged version. Fix: treat the result as an opaque 128-bit value, or generate a real UUIDv7 when you need the tag.
  • Filling the randomness with Math.random. It is predictable and can collide. Fix: always use a CSPRNG like crypto.getRandomValues.
  • Exposing ULIDs publicly without weighing the timestamp leak. See the privacy section above. Fix: internal ULIDs, random UUIDv4 for public IDs.
  • Hand-typing I, L, O, or U into a ULID. Those letters aren’t in the alphabet, and retyping invites errors. Fix: copy ULIDs, don’t retype them.

FAQ

Is ULID an official standard like UUID?

No. ULID is a community specification published on GitHub, not an IETF RFC. It is widely implemented and stable, but it has no standards body behind it. If you need a standardized, time-ordered identifier, UUIDv7 (RFC 9562) applies the same idea inside the official UUID format.

How many characters is a ULID, and why is it shorter than a UUID?

26 characters, versus a UUID’s 36. ULID uses Crockford Base32, which packs 5 bits per character; a UUID’s hexadecimal packs only 4 bits and adds four hyphens. The same 128 bits therefore need fewer characters in Base32 — and none of them need URL escaping.

Can two ULIDs ever collide?

Practically never. Within one millisecond a ULID has 80 random bits — about 1.2 × 10²⁴ possibilities — so even generating millions per millisecond keeps the collision odds vanishingly small. The one requirement is that a cryptographically secure RNG fills the randomness; Math.random voids the guarantee.

Can I store ULIDs in PostgreSQL or MySQL?

Yes. A ULID is 128 bits, so convert it to UUID form and store it in a uuid column (PostgreSQL) or BINARY(16) (MySQL), then render the Base32 string only at the edges. There is no native ULID column type, but the UUID representation costs the same 16 bytes and keeps the index compact.

Are ULIDs case-sensitive?

The canonical form is uppercase, but Crockford Base32 is case-insensitive on input: a decoder reads lowercase letters the same way, and maps I/L to 1 and O to 0. To avoid surprises in equality checks and indexes, normalize to a single case before you store or compare.

Will the 48-bit timestamp ever run out?

Not for a very long time. 48 bits of milliseconds reach the year 10889 before the counter overflows, so the timestamp component is effectively future-proof for any real application. You will replace the system, the language, and the database long before the format runs out of room.

Can I generate ULIDs in the browser or on mobile without a server?

Yes — that’s a core benefit. ULIDs need no central coordinator, so any node, edge worker, browser, or device can mint one from its clock plus a secure RNG. Values created on different machines still sort together by time afterward, because the timestamp lives in the ID itself.

Conclusion

ULIDs solve a specific, real problem — random keys fragmenting your index — without taking away decentralized generation. The mechanics are worth keeping in mind:

  • A ULID is a 48-bit millisecond timestamp + 80 bits of randomness, encoded as 26 Crockford Base32 characters.
  • It sorts by time because the timestamp is the most significant component and the alphabet is order-preserving — string order equals time order.
  • That ordering gives a B-tree the insert locality a random UUIDv4 lacks, keeping writes fast and the index compact.
  • Use monotonic mode when you need strict ordering for IDs minted in the same millisecond.
  • Weigh the timestamp leak before exposing ULIDs on public-facing identifiers.
  • Pick UUIDv7 instead when you must stay inside the standard UUID format.

When you’re ready to put it to work, open the ULID generator to generate, decode, and convert ULIDs entirely in your browser — no server, no upload, nothing stored.

Tags: ulid uuid unique-identifier database primary-key

Related Articles

View all articles