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 a public 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. Good until the year 10895.
  • Randomness (80 bits): cryptographically random. ~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-safeNo (contains hyphens)Yes
Case-sensitiveNoYes (by spec, case-insensitive decoders exist)
SpecRFC 4122ULID 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, finalized 2024) also embeds a millisecond timestamp in the first 48 bits — making it sortable. It’s a direct response to ULID’s success.

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 guarantees ordering even at high throughput (millions of inserts per second).

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 (
    "math/rand"
    "time"
    "github.com/oklog/ulid/v2"
)

ms := ulid.Timestamp(time.Now())
entropy := rand.New(rand.NewSource(time.Now().UnixNano()))
id := ulid.MustNew(ms, entropy)
fmt.Println(id) // "01HQ7V5J3Z2QK8MNRPXTYW9E4B"

Python

pip install python-ulid
from ulid import ULID

id = ULID()
print(str(id))  # "01HQ7V5J3Z2QK8MNRPXTYW9E4B"
print(id.timestamp())  # datetime object

Rust

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

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

Storing ULIDs in a Database

PostgreSQL

-- As text (26 bytes)
CREATE TABLE events (
  id TEXT PRIMARY KEY DEFAULT gen_ulid(),
  ...
);

-- As UUID (16 bytes, loses the string format)
-- Requires a conversion function

PostgreSQL doesn’t natively understand ULID, but you can store as TEXT or convert to BYTEA. 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)); // Wed Apr 15 2026 ...

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 →