About ULID Generator
A ULID is a 128-bit identifier - the same width as a UUID - built as 48 bits of Unix millisecond timestamp followed by 80 bits of randomness, and rendered as 26 characters of Crockford Base32. The point of that layout is that lexicographic sort order matches chronological order: sorting ULIDs as strings sorts them by creation time.
That is what makes them a much better database key than a random UUID v4. Because the leading bits increase over time, new rows append to the end of a B-tree index instead of scattering across it, which avoids the page splits and cache misses that make v4 insert performance degrade as a table grows. You keep decentralised generation - no sequence, no coordination - and gain index locality.
The encoding is deliberately practical. Crockford Base32 omits I, L, O and U, so there are no characters that can be confused with one another or accidentally spell words, it is case-insensitive on input, and 26 characters is shorter than a UUID's 36 while carrying the same 128 bits. It is safe in URLs without escaping.
The trade-off is that a ULID leaks its creation time to anyone holding it - the first 10 characters decode to a millisecond timestamp. That is often useful, and occasionally not: it tells a competitor when an account was created or how many records you generate per second if they can obtain two of them. Where that matters, use a fully random identifier.
Worth knowing: UUID v7 now standardises the same idea - a millisecond timestamp followed by randomness - in the UUID format itself, with RFC 9562 behind it and native support arriving in PostgreSQL 18, .NET 9 and Java 21+ libraries. If you want time-ordered identifiers and do not need the shorter Base32 form, v7 is the better default in 2026 simply because everything already understands a UUID.
How to use the ULID Generator
- Press generate. Each ULID encodes the current millisecond plus 80 fresh random bits.
- Note that ULIDs generated in the same millisecond sort by their random component, not by sub-millisecond arrival order - within one millisecond ordering is arbitrary unless you use a monotonic generator.
- Copy the 26-character value. It is URL-safe as-is and case-insensitive when parsed.
- For storage, keep the 16 raw bytes rather than the 26-character text if index and row size matter.
Examples
-
Sample ULID
01ARZ3NDEKTSV4RRFFQ69G5FAV
ULID Generator in code
The same operation this tool performs, in the languages you are most likely to need it.
import { ulid, monotonicFactory } from "ulid";
ulid(); // "01ARZ3NDEKTSV4RRFFQ69G5FAV"
// Guarantees strictly increasing values within the same millisecond -
// use this if you need a total order, not just a chronological one.
const next = monotonicFactory();
next(); next(); // second is guaranteed > first
// Extract the timestamp from an existing ULID
import { decodeTime } from "ulid";
new Date(decodeTime("01ARZ3NDEKTSV4RRFFQ69G5FAV"));
import ulid
u = ulid.new()
print(str(u)) # 26-char Crockford Base32
print(u.timestamp().datetime)
print(u.bytes) # 16 bytes, for BINARY(16) storage
# Parsing is case-insensitive
ulid.parse("01arz3ndektsv4rrffq69g5fav")
-- Store the 16 bytes, not the 26 characters, when size matters
CREATE TABLE events (
id BINARY(16) PRIMARY KEY, -- MySQL
body JSON
);
-- PostgreSQL: the uuid type is also 16 bytes and works fine for
-- ULIDs, or just use native UUID v7 on PostgreSQL 18+:
CREATE TABLE events (
id uuid PRIMARY KEY DEFAULT uuidv7(),
body jsonb
);
-- Why this matters: with a time-ordered key, inserts append to the
-- end of the index. With random UUID v4 they land everywhere, which
-- causes page splits and a steadily worse insert rate as the table grows.
When you need this
- Primary keys for a table that will grow large, where v4 UUID insert performance is a concern.
- Event or log identifiers where sorting by id should mean sorting by time.
- Identifiers that appear in URLs, where 26 characters beats 36 and there are no ambiguous glyphs.
- Cursor-based pagination, since the id itself is monotonic enough to page on.
- Any place you want UUID-like uniqueness without giving up index locality.
Common problems and what causes them
- Assuming ULIDs from the same millisecond are ordered
- Within a single millisecond the ordering comes from the random component, so it is arbitrary. If you need a strict total order - for cursor pagination, say - use a monotonic factory, which increments the random part instead of re-randomising it.
- Not realising the timestamp is public
- The first 10 characters decode to the creation time in milliseconds. Anyone with the id knows exactly when the record was made, and two ids reveal your creation rate. Use a random identifier where that is sensitive.
- Storing the 26-character text
- That is 26 bytes per value against 16 for the raw bytes, paid again in every index. Use BINARY(16) or a uuid column unless a human reads the column directly.
- Expecting UUID compatibility
- A ULID is 128 bits but its text form is not a UUID, and it does not set the version and variant bits a UUID parser checks. Do not put ULID text in a uuid-typed column expecting it to validate; convert to the 16 bytes, or use UUID v7 instead.
- Timestamp overflow assumptions
- 48 bits of milliseconds runs out in the year 10889, so this is not a practical concern - but note that a system clock moving backwards (NTP correction, VM restore) will produce ULIDs that sort before earlier ones.
FAQ
- ULID or UUID - which should I use?
- If you want time-ordered identifiers, UUID v7 is now the better default: it encodes the same millisecond-plus-randomness layout, is standardised in RFC 9562, and every database and library already understands the UUID type. Choose ULID when the shorter 26-character Base32 form genuinely matters, or where you already have ULIDs in production.
- Why is a ULID better than UUID v4 for a database key?
- Because it sorts chronologically, so inserts append to the end of the index instead of scattering across it. Random v4 keys cause page splits and poor cache locality, and the effect grows with table size - it shows up as insert throughput degrading over months.
- Is a ULID guaranteed to be unique?
- Practically, yes: 80 random bits per millisecond means you would need roughly 1.2 x 10^12 ULIDs in the same millisecond for a 50% collision chance. Within one process, a monotonic generator makes it a guarantee.
- How long is a ULID?
- 26 characters as text, 128 bits (16 bytes) as binary. That is 10 characters shorter than a canonical UUID while carrying the same amount of information, because Base32 is denser than hex-with-hyphens.
- Can I get the creation time out of a ULID?
- Yes - the first 10 characters are a 48-bit millisecond timestamp, and every library exposes a decode function. Which is exactly why you should not use ULIDs where the creation time needs to stay private.
- Are ULIDs case-sensitive?
- No. Crockford Base32 is case-insensitive on input and excludes I, L, O and U to avoid ambiguity. Canonical output is uppercase, but a lowercase ULID parses to the same value.
- ULID vs UUID?
- ULIDs sort chronologically by string order; UUID v4 is random.
Related reading
- ULID vs UUID
- UUID vs ULID vs NanoID
- UUID vs auto-increment keys
- UUID generator random v4
- NanoID generator shorter, no timestamp