What a ULID Actually Is
A ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit identifier written as 26 characters, designed to be unique like a UUID but sortable by creation time. A typical ULID looks like this:
01ARZ3NDEKTSV4RRFFQ69G5FAV
The 128 bits are split into two parts: the first 48 bits are a millisecond Unix timestamp, and the remaining 80 bits are random. Because the timestamp comes first and the encoding preserves byte order, sorting ULIDs as plain strings sorts them by time — which is the whole point.
The 26-Character Format
ULIDs are encoded with Crockford's Base32 (digits 0-9 and letters A-Z, excluding I, L, O, and U to avoid ambiguity). The layout is fixed:
01ARZ3NDEK TSV4RRFFQ69G5FAV
|----------| |----------------|
Timestamp Randomness
48 bits 80 bits
10 chars 16 chars
A few consequences fall out of this:
- ULIDs are case-insensitive and contain no special characters, so they are safe in URLs without encoding.
- The timestamp covers dates until the year 10889, so overflow is not a practical concern.
- 80 bits of randomness means collisions within the same millisecond are astronomically unlikely.
Generating a ULID in Code
Most languages have a small library. In JavaScript:
import { ulid } from 'ulid';
ulid(); // '01ARZ3NDEKTSV4RRFFQ69G5FAV'
// You can also pass an explicit timestamp (ms)
ulid(1469918176385); // '01ARYZ6S41TSV4RRFFQ69G5FAV'
In Python:
from ulid import ULID
str(ULID()) # '01ARZ3NDEKTSV4RRFFQ69G5FAV'
Decoding the timestamp back out is straightforward, because the first 10 characters are just the Base32-encoded milliseconds:
import { decodeTime } from 'ulid';
decodeTime('01ARZ3NDEKTSV4RRFFQ69G5FAV'); // 1469918176385
Monotonic Generation
Within a single millisecond, two plain ULIDs are randomly ordered relative to each other. If you generate many IDs per millisecond and need them strictly increasing, use a monotonic factory, which increments the random component instead of regenerating it:
import { monotonicFactory } from 'ulid';
const next = monotonicFactory();
next(); // 01ARZ3NDEKTSV4RRFFQ69G5FAV
next(); // 01ARZ3NDEKTSV4RRFFQ69G5FAW ← +1, same ms
next(); // 01ARZ3NDEKTSV4RRFFQ69G5FAX
This matters when ULIDs are used as primary keys and you rely on them to reflect insertion order.
ULID vs UUID as a Database Key
The practical reason teams reach for ULIDs is index performance. Random UUIDv4 keys scatter inserts across a B-tree index, causing page splits and poor cache locality. Because ULIDs are time-ordered, new rows append near the "right edge" of the index, much like an auto-increment integer — but without a central sequence and without leaking a row count.
| Property | UUIDv4 | ULID |
|---|---|---|
| Sortable by time | No | Yes |
| Index locality on insert | Poor (random) | Good (sequential) |
| Length as text | 36 chars | 26 chars |
| Reveals creation time | No | Yes (timestamp embedded) |
| URL-safe without encoding | No (hyphens) | Yes |
The one trade-off worth noting: a ULID exposes its creation time to anyone who has it. If a public-facing identifier must not reveal when a record was created, use a random UUID instead.
When to Use ULIDs
ULIDs are a strong default for:
- Primary keys in high-insert tables (orders, events, messages)
- Distributed systems that need client-generated IDs without coordination
- Log and event identifiers where time-ordering aids debugging
- Public IDs where a compact, URL-safe string is preferable to a UUID
Avoid them when creation time must stay private, or when an existing system mandates UUID format.
Frequently Asked Questions
How long is a ULID? 128 bits, written as 26 characters in Crockford Base32 — 10 characters of timestamp followed by 16 characters of randomness.
Are ULIDs guaranteed unique? Not mathematically guaranteed, but with 80 random bits per millisecond a collision is so improbable it can be treated as unique in practice. Use monotonic generation to also guarantee ordering within a millisecond.
Can I get the timestamp back from a ULID?
Yes. The first 10 characters decode to the millisecond Unix timestamp, so no separate created_at column is strictly required.
Are ULIDs better than UUIDs? For time-ordered, index-friendly keys, yes. For identifiers that must not leak creation time, a random UUID is better. They solve different problems.
Use the ULID generator above to create single or bulk ULIDs, inspect the embedded timestamp, and copy them straight into your database or test fixtures.