ULIDs for sortable IDs
Published: 2026-06-26
How ULIDs pack a millisecond timestamp and randomness into 26 Crockford base32 characters, why they sort chronologically as plain text, and when to pick them over UUID v4 or NanoID.
A ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit identifier designed for one property UUID v4 does not offer: chronological sort order as plain text. The first ten characters encode UTC milliseconds; the last sixteen carry cryptographic randomness. The result is a 26-character string in Crockford base32—compact, URL-safe, and index-friendly. If you need opaque random IDs with no ordering hint, UUID v4 is still the default; if you need time-ordered keys without a separate created_at column for sorting, ULIDs are a practical middle ground.
ULID layout in one minute
A canonical ULID looks like:
01ARZ3NDEKTSV4RRFFQ69G5FAV
|----------|----------------|
10 chars 16 chars
timestamp randomness
| Part | Size | Encoding | Role |
|---|---|---|---|
| Timestamp | 48 bits | First 10 characters | Milliseconds since Unix epoch (UTC) |
| Randomness | 80 bits | Last 16 characters | Unpredictable payload from a secure RNG |
Total: 128 bits, same width as a UUID, but the string is shorter (26 vs 36 characters with hyphens) and sorts lexicographically by creation time when IDs are generated in real time.
Under Crockford base32, each character is one of 0-9 and A-Z excluding I, L, O, and U (easy to confuse with 1 and 0). Encoding is case-insensitive: 01arz3ndek… and 01ARZ3NDEK… decode to the same value.
Why lexicographic order matters
Many systems store IDs as strings in databases, logs, and object storage keys. With UUID v4, string sort order is uncorrelated with creation time—you almost always add a timestamp column or a snowflake-style scheme if you want “newest first” in an index or UI.
ULIDs fix that for the common case:
- Same millisecond, different random tail: IDs still sort in a stable, predictable way (timestamp prefix dominates).
- Plain-text sort = time order:
ORDER BY idor sorting log lines by ID approximates chronological order without parsing. - Distributed generation: Each client can mint ULIDs with no central coordinator, as long as clocks are reasonably sane and randomness is strong.
Trade-off: the timestamp is visible to anyone who can read the ID. That is useful for debugging and support; it is not ideal when creation time must stay private. For opaque IDs, stick with UUID v4 or NanoID.
Crockford base32 vs UUID hex
UUIDs are often shown as hex with hyphens (550e8400-e29b-41d4-a716-446655440000). ULIDs use base32 with a restricted alphabet:
0123456789ABCDEFGHJKMNPQRSTVWXYZ
Benefits for ULIDs:
- Shorter strings at the same bit width (128 bits → 26 chars vs 32 hex digits).
- URL- and filename-friendly without extra encoding (no
+,/, or%surprises). - Human-friendlier alphabet (no ambiguous
I/1/O/0).
If you are debugging binary payloads or JWT segments, hex encoding is still the right mental model for the random 80-bit tail once decoded.
ULID vs UUID v4 vs NanoID
| Need | Good fit |
|---|---|
| Sortable by creation time (string or B-tree index) | ULID |
| Standard interop, opaque random ID | UUID v4 — see UUID versions (and when v4 is enough) |
| Shorter or custom alphabet | NanoID (generator) |
| Deterministic ID from a name | UUID v5, not ULID |
| Hide when the row was created | UUID v4 or NanoID, not ULID |
ULIDs are not secrets. The random part adds unpredictability, but the timestamp prefix reveals approximate creation time. Use normal authorization checks; do not rely on ID obscurity.
Randomness and collision risk
The 80 random bits make same-millisecond collisions negligible in practice—far below concerns at typical application scale. Production ULIDs should use a cryptographically secure source:
- Browsers:
crypto.getRandomValues() - Node.js:
crypto.randomBytes()/getRandomValuesequivalents - Avoid
Math.random()for identifiers
The ULID Generator uses getRandomValues for the random segment and the current time for the timestamp prefix.
Clock skew and monotonicity
ULIDs assume millisecond timestamps in the prefix. If system clocks jump backward (NTP correction, VM migration), two IDs generated “later” might sort before older ones. Libraries in some languages offer monotonic ULID variants (incrementing the random part within the same millisecond); the spec’s simple form is timestamp + random per generation call.
For strict global ordering across machines, you may still want a dedicated sequence or database created_at—ULIDs give you good enough ordering for logs, queues, and UX, not a distributed consensus protocol.
Decoding a ULID
You can recover the embedded time without a lookup table:
- Take the first 10 characters and decode from Crockford base32 to an integer.
- That integer is Unix time in milliseconds (UTC).
Example (from the spec’s well-known sample):
01ARZ3NDEKTSV4RRFFQ69G5FAV
^^^^^^^^^^
timestamp → 2016-07-27 17:06:56.754 UTC (illustrative; decode in a tool to verify)
The last 16 characters decode to 80 bits of randomness—often shown as 20 hex digits when debugging.
Paste any valid ULID into the ULID Generator decoder to see UTC, local time, Unix ms, and the random part in hex—all in your browser, with nothing uploaded.
Storing and validating ULIDs
Common practices:
- Column type:
char(26)orvarchar(26)with a case convention (often uppercase); some teams usebinary(16)and convert at the API boundary. - Indexes: String primary keys on ULIDs cluster roughly by time, which can help range scans for “recent rows”—but monitor index bloat like any wide string PK.
- Validation: Exactly 26 characters from the Crockford alphabet; reject unknown symbols.
- APIs: Accept lowercase and uppercase; normalize in logs for consistency.
Invalid length or characters should fail fast at the boundary—same discipline as UUID validation.
Common pitfalls
- Treating sort order as security: Newer IDs sort higher, but guessing valid IDs is still a risk—enforce access control.
- Assuming perfect global order: Clock skew and concurrent generators in the same millisecond mean order is approximate, not a total order guarantee.
- Using ULIDs when time must be secret: The prefix leaks when the ID was minted; use v4 or NanoID for opaque tokens.
- Omitting randomness quality: Weak RNGs undermine the “unique” half of the name; always use Web Crypto or platform CSPRNG.
- Mixing encodings: ULIDs are not UUIDs—do not strip hyphens from a UUID and expect a ULID decoder to work.
When ULIDs shine
Typical fits:
- Database primary keys where recent-first listing matters and a separate sort column is awkward
- Event streams, audit logs, and job IDs where operators grep by time-ish order
- S3-style object keys with natural prefix clustering by day (mind hot partitions at extreme scale)
- Client-generated IDs in offline-first or mobile apps before sync
When length matters more than sortability, compare NanoID sizes and alphabets. When standards compliance with existing UUID tooling matters, stay on UUID v4.
Try it locally in your browser
Use the ULID Generator to:
- Generate single ULIDs or batches (1–500, one per line) with lowercase or uppercase Crockford letters.
- Decode any 26-character ULID to UTC, local time, Unix milliseconds, and the 80-bit random segment as hex.
- Copy results for databases, APIs, or fixtures—processing stays in your browser; nothing is sent to a server.
For opaque random IDs, use the UUID Generator. For shorter custom strings, try the NanoID Generator.
Related reading
- UUID versions (and when v4 is enough) — RFC 4122 layout, when random UUIDs beat time-ordered schemes, and nil UUID notes.
- Hex encoding: UTF-8 bytes explained — useful when inspecting the random tail or other binary payloads as hex.