X Xerobit

ULID Generator — Universally Unique Lexicographically Sortable Identifiers

ULID is a 26-character sortable unique identifier that embeds a millisecond timestamp prefix. Unlike UUID v4, ULIDs sort chronologically. Here's how ULIDs work and when to use...

Mian Ali Khalid · · 5 min read
Use the tool
UUID Generator
Generate UUID v4 and v7 identifiers in bulk.
Open UUID Generator →

ULID (Universally Unique Lexicographically Sortable Identifier) solves a key limitation of UUID v4: random IDs don’t sort chronologically. A ULID embeds a millisecond Unix timestamp in its first 10 characters, so ULIDs generated at different times sort correctly without a separate created_at column.

Use the UUID Generator to generate UUID v4 identifiers.

ULID format

A ULID is a 26-character string using Crockford base32 encoding:

01ARZ3NDEKTSV4RRFFQ69G5FAV
└──────────┘└────────────────┘
  10 chars       16 chars
  48-bit time    80-bit random
  • First 10 characters: 48-bit Unix timestamp in milliseconds
  • Last 16 characters: 80 bits of cryptographic randomness
  • Total: 128 bits (same as UUID)
  • Alphabet: 0123456789ABCDEFGHJKMNPQRSTVWXYZ (Crockford base32 — excludes I, L, O, U to avoid ambiguity)

Example output:

01HVNKQ3RTMWJBQXCZ4GFHPR9Y
01HVNKQ3RTMWJBQXCZ4GFHPR9Z
01HVNKQ3RTMWJBQXCZ4GFHPRA0

These three ULIDs were generated in the same millisecond. They sort correctly because the random component increments lexicographically within the same millisecond.

Generating ULIDs in code

JavaScript

import { ulid, monotonicFactory } from 'ulid';

// Basic generation:
const id = ulid();
// "01HVNKQ3RTMWJBQXCZ4GFHPR9Y"

// With explicit timestamp:
const id = ulid(Date.now());

// Monotonic factory — guaranteed sort order within same millisecond:
const monotonicUlid = monotonicFactory();
const id1 = monotonicUlid();
const id2 = monotonicUlid();
const id3 = monotonicUlid();
// id1 < id2 < id3 (always, even within the same millisecond)

Monotonic mode is important in high-throughput systems where multiple IDs are generated per millisecond. Without it, two IDs in the same millisecond could have the same timestamp prefix and sort randomly by their random component.

Node.js install

npm install ulid

Python

from ulid import ULID

# Generate ULID:
u = ULID()
print(str(u))           # "01HVNKQ3RTMWJBQXCZ4GFHPR9Y"
print(u.timestamp())    # 1709551234.567 (Unix timestamp)
print(u.datetime)       # datetime object

# From existing timestamp:
import datetime
u = ULID.from_datetime(datetime.datetime.now())
pip install python-ulid

Go

package main

import (
    "fmt"
    "github.com/oklog/ulid/v2"
    "math/rand"
    "time"
)

func main() {
    entropy := rand.New(rand.NewSource(time.Now().UnixNano()))
    ms := ulid.Timestamp(time.Now())
    id := ulid.MustNew(ms, entropy)
    
    fmt.Println(id.String())  // "01HVNKQ3RTMWJBQXCZ4GFHPR9Y"
    fmt.Println(ulid.Time(id.Time()))  // time.Time
}

Extracting the timestamp from a ULID

import { decodeTime } from 'ulid';

const id = '01HVNKQ3RTMWJBQXCZ4GFHPR9Y';
const timestamp = decodeTime(id);
// 1709551234567 (milliseconds since Unix epoch)

const date = new Date(timestamp);
// "2024-03-04T14:20:34.567Z"

This is a key ULID advantage: you can extract the creation time from the ID itself without a separate database column.

ULID in databases

PostgreSQL

-- Store as CHAR(26) or TEXT:
CREATE TABLE events (
    id CHAR(26) DEFAULT '' NOT NULL,
    type TEXT NOT NULL,
    payload JSONB,
    PRIMARY KEY (id)
);

-- With application-generated ULIDs:
INSERT INTO events (id, type, payload) 
VALUES ('01HVNKQ3RTMWJBQXCZ4GFHPR9Y', 'user.created', '{"user_id": 1}');

-- Sorting by ID = sorting by creation time:
SELECT * FROM events ORDER BY id DESC LIMIT 10;

-- Range queries by time using ULID prefix:
-- All events from March 2024:
SELECT * FROM events 
WHERE id >= '01HWKM00000000000000000000'
  AND id <  '01HXABG00000000000000000000';

Index performance vs UUID

UUID v4 causes B-tree index fragmentation because new values insert randomly throughout the tree. ULIDs insert near the end of the index (newest values are largest), which is the same access pattern as auto-increment integers — much better for insert performance on large tables.

-- Benchmark insert performance (PostgreSQL, 10M rows):
-- UUID v4 primary key: ~82,000 inserts/sec
-- ULID primary key:    ~94,000 inserts/sec  (+15%)
-- BIGSERIAL (int8):    ~97,000 inserts/sec  (+18%)

ULID vs UUID comparison

PropertyUUID v4ULID
SortableNoYes (chronological)
Length36 chars26 chars
AlphabetHex + dashesCrockford base32
Timestamp in IDNoYes (ms precision)
StandardRFC 4122ULID spec
DB native typeYes (Postgres UUID)No (store as CHAR)
Library supportUniversalGood (major languages)
Entropy122 bits80 bits random + 48 bits time

ULID vs KSUID

KSUID (K-Sortable Unique Identifier) is an alternative sortable ID format:

PropertyULIDKSUID
Length26 chars27 chars
Timestamp precisionMillisecondsSeconds
Timestamp epochUnix (1970)Custom (2014)
Random bits80 bits128 bits
EncodingBase32Base62

KSUID has more random bits but second-level precision (not millisecond). ULID is better for most use cases.

UUID v7 — the standardized version of ULID

RFC 9562 (2024) introduced UUID v7, which is essentially ULID in UUID format:

UUID v7: 018e7b5a-5218-7xxx-yxxx-xxxxxxxxxxxx
          └──────────────┘
          48-bit ms timestamp

UUID v7:

  • Same chronological sorting as ULID
  • Standard UUID format (36 chars, UUID type in databases)
  • Becoming available in libraries and databases (PostgreSQL 17 supports it)

For new projects where database UUID type support matters, UUID v7 may be preferable to ULID. For projects already using ULID or needing the Crockford base32 format, ULID remains a solid choice.


Related posts

Related tool

UUID Generator

Generate UUID v4 and v7 identifiers in bulk.

Written by Mian Ali Khalid. Part of the Dev Productivity pillar.