UUID is the default unique identifier for most applications. But UUID v4 — random by design — is unordered. Rows inserted into a database aren’t stored in insertion order. Indexes fragment. Queries slow down. ULID was designed to fix exactly this problem.

Generate a ULID →

What Is a ULID?

ULID stands for Universally Unique Lexicographically Sortable Identifier. It was created by Alizain Feerasta in 2016 and is defined in the ULID spec.

A ULID looks like this:

01HQ7V5J3Z2QK8MNRPXTYW9E4B

26 characters, uppercase, using Crockford’s Base32 alphabet (excludes I, L, O, U to avoid visual ambiguity).

The structure

 01HQ7V5J3Z  2QK8MNRPXTYW9E4B
|-----------|-----------------|
  timestamp     randomness
  48 bits         80 bits
  • Timestamp (48 bits): milliseconds since Unix epoch. The spec says it won’t run out of space until the year 10889 AD (2⁴⁸ − 1 ms is 10889-08-02 UTC).
  • Randomness (80 bits): from a cryptographically secure source where possible (spec wording). ~1.2 × 10²⁴ possible values per millisecond.

ULID vs UUID: Key Differences

FeatureUUID v4ULID
Length36 chars (with hyphens)26 chars
SortableNoYes (lexicographic)
Timestamp embeddedNoYes
Collision probabilityExtremely lowExtremely low
URL-safeYes (hex digits and hyphens are unreserved in RFC 3986)Yes
Case-insensitiveYesYes (by spec)
SpecRFC 9562 (obsoletes RFC 4122)ULID spec

The critical difference: ULIDs sort by creation time. Insert 1000 rows in a ULID-keyed table and the primary key index stays sequential. This matters for:

  • B-tree indexes: Sequential inserts hit the same page, reducing fragmentation.
  • Pagination: You can use WHERE id > :cursor as an efficient cursor without a separate created_at column.
  • Log files: Sorted by filename = sorted by time.
  • Message queues: Natural ordering without a separate sequence number.

ULID vs UUID v7

UUID v7 (RFC 9562, published May 2024) also embeds a millisecond timestamp in the first 48 bits — making it sortable. Section 2.1 of the RFC lists ULID among the time-ordered ID schemes that motivated the new versions.

FeatureULIDUUID v7
Format26-char Base3236-char hex with hyphens
RFC standardNoYes (RFC 9562)
DB native typeTEXT or BINARYUUID type (PostgreSQL)
Spec maturityStable community specNew RFC

If your database has native UUID support (PostgreSQL’s uuid type), UUID v7 may be more convenient — it stores in 16 bytes natively. If you’re working with string-based systems or want shorter IDs, ULID is still the better choice.

Monotonicity Within the Same Millisecond

When multiple ULIDs are generated in the same millisecond, the timestamp portion is identical. The ULID spec defines a monotonicity extension: increment the random component by 1 for each subsequent ULID within the same millisecond. This keeps IDs from one generator in order. If the random component overflows within one millisecond, generation fails. IDs made by different processes in the same millisecond are still ordered only by their random bits.

The ZeroTool generator applies this extension, like monotonicFactory() from the ulid package: within one millisecond each ULID adds 1 to the random part of the one before, so a batch is listed in sorted order, and generation stops with an error if the random part would overflow.

Generating ULIDs in Code

JavaScript / TypeScript

npm install ulid
import { ulid } from 'ulid';

const id = ulid();
// "01HQ7V5J3Z2QK8MNRPXTYW9E4B"

// With a custom timestamp
const id2 = ulid(Date.now());

Go

go get github.com/oklog/ulid/v2
import (
    "crypto/rand"
    "fmt"

    "github.com/oklog/ulid/v2"
)

id := ulid.MustNew(ulid.Now(), rand.Reader)
fmt.Println(id) // "01HQ7V5J3Z2QK8MNRPXTYW9E4B"

ulid.Make() (v2.1+) is shorter and monotonic within a millisecond, but its default entropy is math/rand seeded from the clock, so use crypto/rand when IDs must be hard to guess.

Python

pip install python-ulid
from ulid import ULID

id = ULID()
print(str(id))  # "01HQ7V5J3Z2QK8MNRPXTYW9E4B"
print(id.timestamp)  # Unix seconds as a float
print(id.datetime)   # timezone-aware datetime (UTC)

Rust

[dependencies]
ulid = "1"
use ulid::Ulid;

let id = Ulid::new();
println!("{}", id); // "01HQ7V5J3Z2QK8MNRPXTYW9E4B"

Storing ULIDs in a Database

PostgreSQL

-- As text (26 bytes); the application generates the ULID
CREATE TABLE events (
  id CHAR(26) PRIMARY KEY,
  ...
);

-- As UUID (16 bytes, loses the string format)
-- Convert in the application, e.g. ulidToUUID() from the ulid npm package

PostgreSQL has no built-in ULID type or generator function (a DEFAULT needs an extension), so most teams generate the ID in the application and store it as TEXT, BYTEA or uuid. Many teams use TEXT for clarity; the 26-byte cost is negligible compared to the query simplicity.

MySQL / SQLite

Same approach — store as CHAR(26) or VARCHAR(26). It’s a fixed-width string, so CHAR(26) is slightly more efficient.

Decoding a ULID

The timestamp is embedded in the first 10 characters. You can extract it:

import { decodeTime } from 'ulid';

const ts = decodeTime('01HQ7V5J3Z2QK8MNRPXTYW9E4B');
console.log(new Date(ts).toISOString()); // 2024-02-22T07:23:36.959Z

This is useful for debugging: you can tell exactly when a record was created from its ID alone, without querying created_at.

When to Use ULID

  • Any table with high insert rates where index fragmentation is a concern
  • Event logs, audit trails, or any append-heavy workload
  • Systems where you need time-ordered IDs without a separate timestamp column
  • APIs where you want URL-safe, human-readable IDs shorter than UUID

Generate a ULID now →