ULID Generator

Generate ULIDs — 26-character identifiers that sort by creation time because the time is literally at the front of the string. This generator implements the specification properly: a 48-bit millisecond timestamp, 80 bits of cryptographic randomness, Crockford base32, and a monotonic counter so a batch created inside one millisecond still comes out in order. The decoder reads the timestamp back out.

Randomness comes from crypto.getRandomValues and never leaves the page. In monotonic mode a repeated millisecond increments the 80-bit random field by one rather than redrawing it, which is what keeps a same-millisecond batch in creation order.

Press Generate to create ULIDs.
Your generated ULIDs will appear here.

What a ULID is made of

A ULID — Universally Unique Lexicographically Sortable Identifier — is 128 bits split into two fields and written as 26 characters. The first 48 bits are the Unix timestamp in milliseconds, big-endian. The remaining 80 bits are random. That is the whole specification, and its consequences all follow from the ordering of those two fields.

Written out, a ULID looks like 01ARZ3NDEKTSV4RRFFQ69G5FAV. The first ten characters encode the timestamp and the last sixteen encode the randomness. Because base32 maps a fixed number of bits to each character in the same order as the underlying bytes, comparing two ULIDs as ordinary strings gives the same answer as comparing the 128-bit integers they represent — and since time is in the high bits, that is the same answer as comparing creation times. Sorting a list of ULIDs alphabetically sorts them chronologically. No parsing, no separate column, no index on created_at.

The 26-character arithmetic

Base32 carries five bits per character, so 26 characters carry 130 bits — the smallest multiple of five that covers 128. The two spare bits sit at the very top of the first character, which is why a valid ULID never begins with a character above 7: the encodable range stops at 7ZZZZZZZZZZZZZZZZZZZZZZZZZ. A value beginning with 8 or higher has overflowed 128 bits and is not a ULID at all, no matter how well-formed it looks. This is one of the checks the decoder above performs.

The timestamp field is 48 bits, which counts milliseconds up to the year 10889. That is deliberately generous — the field will not run out the way a 32-bit seconds counter does in 2038 — and the ten leading characters of every ULID generated in the same era share a long common prefix, which is why a batch generated together looks so visually similar.

Crockford base32, and why four letters are missing

ULIDs use Douglas Crockford's base32 alphabet: 0123456789ABCDEFGHJKMNPQRSTVWXYZ. Four letters are absent. I, L and O are excluded because they are visually confusable with 1 and 0 — a distinction that vanishes entirely when someone reads an identifier over the phone, copies it off a screenshot in a support ticket, or types it from a printed invoice. U is excluded so that a random identifier is less likely to spell something unfortunate.

That alphabet is the practical difference between a ULID and a hex-encoded identifier of the same size. Thirty-two symbols instead of sixteen means fewer characters for the same entropy, and the exclusions mean the string survives being handled by humans. The trade-off is that ULIDs are conventionally uppercase, which some people find shoutier in a URL than the lowercase hex of a UUID. Crockford's decoding rules are case-insensitive, so a lowercase ULID decodes correctly — but your database's string comparison almost certainly is not, so normalise the case at the boundary and store exactly one form.

Monotonicity: the part most implementations get wrong

A naive ULID generator draws 80 fresh random bits every time. That is fine across milliseconds, because the timestamp prefix advances and dominates the comparison. It falls apart inside a millisecond. A modern process can create thousands of identifiers in one millisecond, and all of them share the same ten-character prefix, so their relative order is decided entirely by the random tail — which is to say, decided at random.

For most uses that does not matter. For the uses ULIDs are chosen for, it does. If you are writing an event log and reading it back in identifier order, a batch inserted in the same millisecond comes back shuffled. If you are paginating with a keyset cursor — "give me the next hundred rows after this ID" — two rows created in the same millisecond can straddle the cursor, and one of them is silently skipped or returned twice. The bug is rare, non-deterministic, and appears only under load, which makes it unusually expensive to find.

The specification's answer is a monotonic generator, and it is what the Monotonic option above implements. The generator remembers the last timestamp and the last 80-bit random value. When a new call arrives in the same millisecond, instead of drawing new randomness it increments the previous random field by one, treated as an 80-bit big-endian integer. Consecutive values in the same millisecond therefore differ by exactly one in the low bits and sort in creation order by construction. When the millisecond advances, fresh randomness is drawn.

Two details make the implementation non-trivial in JavaScript. First, 80 bits do not fit in a Number, so the increment must be done as a carry propagation across a byte array from the least significant byte upward — not as arithmetic on a float. Second, the increment can overflow: if all 80 bits are already ones, there is no next value in that millisecond. The generator here waits for the clock to tick rather than emitting a value that would sort backwards. The odds of reaching that state are negligible, but "negligible" and "handled" are different things, and an overflow that silently wraps would produce a duplicate identifier.

A third case is a clock that moves backwards, which happens on NTP corrections, virtual machine migrations and laptop resume. If the current time is earlier than the last time seen, this generator keeps using the last timestamp and continues incrementing, so ordering is preserved through the correction. Try it: switch to Monotonic, generate 1000 values, and check that they are already sorted — then switch to Plain and generate 1000 more, and you will see the same-millisecond runs come out shuffled.

ULID vs UUID v7

These two solve the same problem the same way, and choosing between them is mostly about the ecosystem you already have rather than about the identifiers themselves. Both are 128 bits. Both put a 48-bit Unix millisecond timestamp in the most significant position. Both fill the rest with randomness and both are lexicographically sortable in their canonical text forms. For the full picture of the UUID family — v1, v4, v7, the version and variant bits, and how random keys fragment an index — see the UUID generator, which is this site's reference page for UUIDs.

ULIDUUID v7
Total bits128128
Timestamp48-bit Unix ms, leading48-bit Unix ms, leading
Bits available for dataAll 128122 — six are spent on version and variant
Text encodingCrockford base32, 26 chars, uppercaseHex with hyphens, 36 chars, lowercase
Ambiguous charactersNone — I, L, O, U removedNone, but hex offers no such protection by design
Standardised byA community specification on GitHubIETF RFC 9562
Native database columnNo — store as 16 bytes or CHAR(26)Yes, wherever a uuid type exists
MonotonicityDefined in the spec as an optional generator behaviourOptional use of the 12-bit rand_a field as a counter

The honest summary: if your stack already speaks UUID, use v7. Every ORM, every validation regex, every log parser and every uuid column already handles it, and RFC 9562 settles arguments that a community specification cannot. Reach for ULID when the identifier is going to be handled by people — printed on a receipt, quoted in a support conversation, typed into a search box, embedded in a short URL. Twenty-six unambiguous characters beat thirty-six hex digits and four hyphens in every one of those situations.

One conversion caveat, because it bites people. A ULID's 128 bits can be rewritten as UUID text — the hex form option above does exactly that — but the result is not a valid UUID. UUID version and variant live at fixed bit positions, and a ULID has real data in those positions, so a strict parser will read the version nibble as whatever the timestamp and randomness happened to put there. Round-trip the bytes if you need to store a ULID in a uuid column, but do not expect the text form to pass UUID validation, and do not mix the two representations in one column.

Where the sortability pays off

The reason to want a time-ordered identifier at all is almost always the database. A B-tree index over a random key scatters every insert into a different leaf page: pages split in the middle, fill factors drop, the whole index becomes the working set, and write throughput degrades as the table grows. A time-ordered key appends at the right-hand edge instead, so one page stays hot, splits are clean and index size stays proportional to the data.

Beyond raw insert performance, the time prefix buys several things for free:

  • Range scans by time without a second index. "Everything created after 09:00" becomes a comparison against a synthetic ULID built from that timestamp with a zero random field — which is what the Generate at time box above is for.
  • Keyset pagination that is stable under concurrent writes. A cursor of "after this ULID" cannot skip or repeat rows the way an OFFSET can, provided the generator is monotonic.
  • Debuggability. During an incident you can read a rough creation time straight out of an identifier in a log line, with no database access. Paste one into the decoder above.
  • Deterministic fixtures. Generating at a fixed timestamp produces test data whose ordering you control, which makes assertions about sort order meaningful instead of flaky.
  • Sharding by time. The prefix is a natural partition key for cold-storage rollups.

The cost is the mirror image of the benefit: a ULID publishes its creation time to anyone who holds it. For an internal primary key that is fine. For a public identifier it may not be — a signup ID that reveals the exact millisecond of registration tells a competitor your growth rate, and two ULIDs side by side tell anyone which record came first. Where that matters, keep a ULID internally and expose a random identifier such as a NanoID or a UUID v4 externally.

Practical notes for API and database work

Storage. A ULID is 16 bytes. Store it as BINARY(16) in MySQL, as bytea or a uuid column in PostgreSQL, or as a 16-byte BLOB in SQLite. CHAR(26) is acceptable and often chosen for readability, and it is still 30% smaller than the 36-character UUID text form — but it is 62% larger than the binary form, and in a clustered index such as InnoDB's that cost is paid again in every secondary index.

On the wire. Send the 26-character canonical uppercase string in JSON. Binary is a storage decision, not a wire format; a base64-encoded blob in an API response helps nobody. Use the JSON formatter to check a payload full of them, and the API tester to send one to an endpoint and watch what comes back.

Validation. A correct check is 26 characters, all from the Crockford alphabet, with a first character of 07. A regex of [0-9A-Z]{26} is wrong: it accepts I, L, O and U, which no generator produces, and it accepts values that overflow 128 bits. Reject those at the edge and answer 400 Bad Request rather than letting a malformed identifier reach a query.

Idempotency and correlation. A ULID works well as an Idempotency-Key or a request correlation ID, with the bonus that the identifier itself carries the time the request started — see the HTTP headers guide for where those headers belong. If you need a unique token identifier for a JWT's jti claim, the JWT generator can mint one alongside the token.

Language support. Every major ecosystem has a ULID library, and the ones worth choosing expose the monotonic factory explicitly rather than hiding it. If you are auditing a dependency, the two questions to ask are whether it uses a cryptographic random source and whether repeated calls inside one millisecond are ordered. A library that fails either is producing something that only looks like a ULID.

Frequently asked questions

What is a ULID?

A Universally Unique Lexicographically Sortable Identifier: 128 bits made of a 48-bit Unix millisecond timestamp followed by 80 random bits, written as 26 Crockford base32 characters. Because time occupies the high bits, sorting ULIDs as strings sorts them by creation time.

Why is a ULID 26 characters long?

Base32 carries 5 bits per character and 26 characters carry 130 bits — the smallest multiple of 5 covering 128. The two spare bits sit at the top of the first character, which is why a valid ULID never starts above 7.

Which letters does Crockford base32 leave out?

I, L, O and U. The first three collide visually with 1 and 0 when a value is read aloud or typed from a screen; U is dropped to make accidental words less likely.

What does monotonic generation mean?

Inside one millisecond the timestamp does not change, so plain generation orders values randomly. A monotonic generator increments the previous 80-bit random field by one instead of redrawing it, which keeps same-millisecond values in creation order.

Is a ULID the same as a UUID v7?

Structurally very close, but not interchangeable. UUID v7 spends six bits on version and variant and is written as 36 hex characters; a ULID uses all 128 bits and is written as 26 base32 characters. See the UUID generator for the full UUID reference.

Does a ULID reveal when it was created?

Yes, to the millisecond, and no key is needed to read it. Useful for internal keys and debugging; wrong for a public identifier whose creation time is sensitive.

Can two ULIDs collide?

Only within the same millisecond, since different milliseconds have different prefixes. With 80 random bits, a fifty percent collision chance needs roughly 1.2 trillion values in that one millisecond — and monotonic generation removes the risk entirely for values from one generator.

How should I store a ULID?

As 16 raw bytes where possible — BINARY(16), bytea or a BLOB — and as CHAR(26) otherwise. Either way the ordering keeps index inserts at the right-hand edge of the B-tree.

Are ULIDs case sensitive?

The canonical form is uppercase and Crockford decoding is case-insensitive, but database string comparison usually is not. Normalise to uppercase at the boundary and store one form.

Related tools and reference