ULID generator
Lexicographically sortable 128-bit identifiers: a millisecond timestamp plus 80 random bits, in 26 Crockford Base32 characters.
What a ULID is
A ULID is 128 bits written as 26 characters. The first 48 bits are a Unix timestamp in milliseconds, big-endian, and the remaining 80 bits are random. Because the timestamp sits in the most significant bits and the encoding is fixed-width and big-endian, sorting ULIDs as plain text sorts them by creation time. No type-aware comparison, no separate created_at column in the sort key, no parsing.
The encoding is Crockford Base32, exactly 0123456789ABCDEFGHJKMNPQRSTVWXYZ. That is the digits and the uppercase letters with I, L, O and U removed. Crockford states the reasons: I and L "Can be confused with 1", O "Can be confused with 0", and U is excluded for "Accidental obscenity". Thirty-two symbols is five bits per character with nothing wasted, which is how 128 bits fit into 26 characters against a UUID's 36.
The 48-bit timestamp runs out at 2^48 - 1 milliseconds after the Unix epoch, which is 281,474,976,710,655 ms, or 8,919.59 years, landing in the year 10889. Decoding is case-insensitive and the canonical output is uppercase, so a ULID survives being read aloud, typed back in, or passed through a system that changes case. It contains no hyphens, so a double-click selects the whole value.
| Characters | Field | Contents |
|---|---|---|
| 0-9 | Timestamp | 48-bit big-endian Unix time in milliseconds |
| 10-25 | Randomness | 80 bits, from a CSPRNG |
Monotonicity, and what it costs
The timestamp only resolves to the millisecond, so two ULIDs created inside the same millisecond are separated by randomness alone and their relative order is arbitrary. The specification is blunt about the default: "Within the same millisecond, sort order is not guaranteed".
Monotonic mode fixes that. When the same millisecond is detected, the random component is incremented by one bit in the least significant position, with carrying, so consecutive values inside one tick still sort in creation order. The specification also states the failure mode: if you generate more than 2^80 ULIDs in a millisecond, or the random component otherwise overflows, generation must fail rather than wrap. The practical headroom is 2^80 minus whatever the first random draw of that millisecond happened to be, so an unlucky first draw near the top of the range leaves less room than the headline number suggests. It is still astronomically unlikely, but it is a real error path your code should handle rather than assume away.
Monotonic mode has a security consequence the specification does not mention. Inside a burst, each value is the previous value plus one, so anyone holding one identifier can compute its neighbours exactly. If monotonic ULIDs are handed out as public identifiers, adjacency leaks. Use non-monotonic ULIDs for anything externally visible, or do not use a ULID for that value at all.
A 26-character Base32 string can hold 130 bits; a ULID holds 128. The largest valid ULID is therefore 7ZZZZZZZZZZZZZZZZZZZZZZZZZ, and any value whose first character is above 7 is out of range. Implementations are required to reject it rather than overflow.
ULID against UUIDv7, honestly
These two formats solve the same problem with the same design. ULID came first, and RFC 9562 lists it first among the sixteen prior-art implementations the working group analysed while drafting version 7. UUIDv7 is 48 bits of Unix millisecond timestamp in the most significant bits and 74 random bits filling the rest, which is ULID's layout minus the six bits that RFC 9562 spends on the version and variant fields.
What UUIDv7 has that ULID does not is standing. It is an IETF Standards Track RFC, final since May 2024. It drops into an existing uuid column, ORM and language standard library with no new dependency and no conversion. PostgreSQL 18 ships uuidv7(), .NET 9 has Guid.CreateVersion7(), and the major Go, Java, Python and Rust libraries implement it. ULID has none of that: it is not a standard, no database has a native type for it, and the storage answer is 16 raw bytes with rendering done in the application.
What ULID has is ergonomics. Twenty-six characters against thirty-six. No hyphens, so it selects with one double-click and survives being tokenised by software that splits on punctuation. No I, L, O or U, so a person can read one off a printed page. Case-insensitive on decode. Those are real properties and UUIDv7 has none of them.
Its maintenance position is the honest caveat. The five most recent commits to the ULID specification repository are all dated 2019-05-23, and issue #91, "Deprecate in favor of UUIDv7", has been open since October 2023 with thirteen comments and no resolution. That said, the npm package still pulls about 10.8 million downloads a week, so calling ULID dead would be wrong. The specification is frozen; the implementations are not.
The practical recommendation: if you want a standard that your database, your ORM and every future engineer on the team already understand, use UUIDv7. If you control the storage and the shortest sortable string is worth something to you, ULID is still a reasonable choice. If you want both, generate a UUIDv7 and render it in Crockford Base32 for display.
| Format | Characters | Random bits | Sorts by time | In a URL | Reveals creation time | Fits a uuid column |
|---|---|---|---|---|---|---|
| ULID | 26 | 80 | Yes, to the millisecond | Yes, alphanumeric, no hyphens | Yes, to the millisecond | After conversion |
| UUID v7 | 36 | 74 | Yes, to the millisecond | Yes, 32 hex digits and 4 hyphens | Yes, to the millisecond | Yes |
| UUID v4 | 36 | 122 | No | Yes, 32 hex digits and 4 hyphens | No | Yes |
| KSUID | 27 | 128 | Yes, to the second | Yes, alphanumeric, no hyphens | Yes, to the second | No |
| Nano ID (21) | 21 | 126 | No | Yes, no escaping. Can start with - or _. | No | No |
Two things reference implementations got wrong
The JavaScript reference implementation shipped a genuine distribution bug until late 2025. Its random source divided by 0xff instead of 0x100, producing a value in [0, 1] rather than [0, 1), which made the first character of the alphabet roughly 29 percent more likely than the last. Pull request #128 fixed it, merged on 2025-11-22, and the fix shipped in release 3.0.2 on 2025-11-30. This is worth knowing for two reasons: it is an off-by-one in a library doing about 10.8 million downloads a week that survived six years, and the ULID specification only says a cryptographically secure source should be used "if possible", which is how a Math.random-shaped interface ended up in the reference code at all.
The second is the UUID conversion. The reference ulidToUUID function decodes 26 Crockford characters to 16 bytes, renders them as hex and inserts hyphens. It does not set the RFC 9562 version nibble or the variant bits, so the output is a valid 128-bit UUID-shaped string but usually not a valid UUID of any version: the version nibble is whatever random bits happened to land there, and the variant bits are wrong three times in four, because only one of the four possible top-bit pairs is the 10 that RFC 9562 requires. A format check on the hyphens and the hex will pass it. Code that switches on the version will not behave. The conversion in the other direction has the mirror-image problem. Every 128-bit value encodes to a syntactically valid ULID, because 26 Crockford characters hold 130 bits and the leading character is therefore always in the range 0 to 7, so nothing rejects it. What you get is a ULID whose first 48 bits are not a timestamp at all: reinterpret a version 4 UUID as a ULID and it decodes to a uniformly random instant somewhere in the next 8,900 years.
Both of these are reasons to validate rather than assume. Every code sample below reads the timestamp back out of the value it just made, because the way to know a ULID carries the time you think it does is to decode it rather than to trust the format.
Doing this in your own code
JavaScript
import { ulid, monotonicFactory, decodeTime } from "ulid";
ulid(); // 01ARZ3NDEKTSV4RRFFQ69G5FAV
decodeTime(ulid()); // milliseconds since the Unix epoch
const next = monotonicFactory();
next(); next(); // second value is first value plus one
// Without the dependency. 10 timestamp characters, 16 random
// characters, Crockford Base32.
const B32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
function ulidLocal(now = Date.now()) {
let time = "";
for (let i = 0; i < 10; i++) {
time = B32[now % 32] + time;
now = Math.floor(now / 32);
}
const bytes = crypto.getRandomValues(new Uint8Array(16));
let rand = "";
// 32 divides 256, so this mask is exactly uniform: 5 bits per
// character, 16 characters, 80 random bits.
for (let i = 0; i < 16; i++) rand += B32[bytes[i] & 31];
return time + rand;
} The JavaScript reference implementation shipped a real bias until release 3.0.2 in November 2025. Check the version you have pinned.
Python
# pip install python-ulid
from ulid import ULID
u = ULID()
str(u) # 01ARZ3NDEKTSV4RRFFQ69G5FAV
u.datetime # decoded creation time
u.to_uuid() # 128 bits reinterpreted, see the caveat below
ULID.from_str("01ARZ3NDEKTSV4RRFFQ69G5FAV") A ULID converted to UUID form does not carry a valid RFC 9562 version nibble unless the library sets one.
Go
import (
"crypto/rand"
"time"
"github.com/oklog/ulid/v2"
)
// Non-monotonic: fresh randomness on every call.
id := ulid.MustNew(ulid.Timestamp(time.Now()), rand.Reader)
// Monotonic: values inside one millisecond still sort in order.
entropy := ulid.Monotonic(rand.Reader, 0)
id = ulid.MustNew(ulid.Timestamp(time.Now()), entropy)
ulid.Time(id.Time()) // decoded creation time Pass crypto/rand.Reader, not math/rand. The ULID specification only says a CSPRNG should be used "if possible".
Java
// com.github.f4b6a3:ulid-creator:5.2.3
import com.github.f4b6a3.ulid.UlidCreator;
UlidCreator.getUlid(); // random component
UlidCreator.getMonotonicUlid(); // increments within a millisecond
UlidCreator.getUlid().getTime(); // milliseconds since the Unix epoch PostgreSQL
-- There is no native ULID type. The usual pattern is to store
-- the 16 bytes and render them on the way out.
CREATE TABLE events (
id bytea PRIMARY KEY, -- 16 raw bytes, not char(26)
body jsonb NOT NULL
);
-- If you want a standard the database already understands,
-- PostgreSQL 18 has this instead:
SELECT uuidv7(); Both are varlena types with a 1-byte header on short values, so char(26) costs 27 bytes on disk against 17 for the 16 raw bytes, and gives up nothing in return. A uuid column is a fixed 16.
Shell
# Validate rather than generate: a ULID is 26 Crockford Base32
# characters and the first one cannot exceed 7.
grep -E '^[0-7][0-9A-HJKMNP-TV-Z]{25}$' <<< "01ARZ3NDEKTSV4RRFFQ69G5FAV"
# 26 base32 characters can hold 130 bits; a ULID holds 128.
# Anything from 8ZZZ... upward is out of range and must be rejected. Questions
Should I use ULID or UUIDv7?
UUIDv7 unless you have a specific reason not to. It is an IETF standard, it stores in a native uuid column, and PostgreSQL 18, .NET 9 and the major language libraries generate it without a dependency. Choose ULID when the 26-character Crockford Base32 form is worth something to you: a shorter string, no hyphens, no look-alike letters, case-insensitive decoding. The two carry the same 48-bit millisecond timestamp either way.
Is a ULID a UUID?
It is the same 128 bits, and it converts to UUID text form byte for byte. It is not a valid RFC 9562 UUID, because the conversion does not set the version and variant fields, so the result has an arbitrary version nibble. If you need a value that passes as a UUID of a known version, generate a UUID.
Are ULIDs case sensitive?
Canonical output is uppercase and decoding accepts either case. The reference validation regex matches both cases and rejects I, L, O and U outright in either case. Note that it does not implement Crockford's optional substitutions, so it will not read an O as a zero or an I as a one; it simply refuses them.
Can I use a ULID as an API key or session token?
No. Only 80 of the 128 bits are random, and the other 48 are a timestamp that anyone holding the value can read. Eighty bits is defensible against guessing, but a token that publishes its own creation time is the wrong shape for a secret, and in monotonic mode consecutive values differ by one. Use a Nano ID of 22 characters or raw random bytes instead.
Is ULID still maintained?
The specification is frozen. Its five most recent commits are all dated 2019-05-23 and an issue proposing deprecation in favour of UUIDv7 has been open since October 2023 without resolution. The implementations are active: the JavaScript library shipped a bias fix in release 3.0.2 in November 2025 and the npm package still does roughly 10.8 million downloads a week.
What happens to ULIDs after the year 10889?
The 48-bit millisecond timestamp saturates at 2^48 - 1 milliseconds after the Unix epoch, which is 8,919.59 years, so the last representable instant falls in the year 10889. Any encoding past that point exceeds the 128-bit value and must be rejected rather than wrapped.