Universally Unique Lexicographically Sortable Identifier (ULID) Analysis

Overview

ULID is a community-driven specification for unique identifiers that combine the decentralized generation of UUIDs with the performance benefits of time-ordered sequential IDs. Created as an alternative to UUID v4’s poor database performance, ULID predates UUID v7 but shares similar design goals.

URI Safety

✅ Completely URI-Safe

ULIDs are designed with URI usage as a primary consideration.

Character set:

  • Uses Crockford’s Base32 alphabet
  • Characters: 0123456789ABCDEFGHJKMNPQRSTVWXYZ
  • Excluded: I, L, O, U (avoid confusion and potential abuse)
  • 32 unique characters

Format:

01ARZ3NDEKTSV4RRFFQ69G5FAV

Characteristics:

  • 26 characters (10 timestamp + 16 randomness)
  • No hyphens (unlike UUID’s 36 chars with hyphens)
  • Case-insensitive (can be normalized)
  • More compact than UUID string representation

Advantages over UUID:

  • Shorter (26 vs 36 characters)
  • No special characters required
  • More human-readable
  • Case-insensitive (easier to communicate verbally)

Usage in URIs:

/api/users/01ARZ3NDEKTSV4RRFFQ69G5FAV
?id=01ARZ3NDEKTSV4RRFFQ69G5FAV

Database Storage and Performance

Storage Size

Binary representation:

  • 128 bits = 16 bytes
  • Same as UUID

String representation:

  • 26 characters
  • As UTF-8 string: 26 bytes minimum
  • As MySQL CHAR(26) with utf8mb4: 72 bytes
  • Recommendation: Store as binary (16 bytes) for optimal efficiency

Storage comparison:

FormatSizeEfficiency
Binary (BYTEA/BINARY(16))16 bytesOptimal
String (CHAR(26))26+ bytes1.6× larger
UUID string (CHAR(36))36+ bytes2.25× larger

Index Performance

ULIDs provide significant performance advantages over random identifiers:

B-tree Index Benefits

Sequential insertion pattern:

  • ✅ Dramatically reduces page splits vs UUID v4
  • ✅ Minimizes write amplification
  • ✅ Improves cache utilization
  • ✅ Reduces I/O operations
  • ✅ Prevents index fragmentation and bloat

Recent benchmarks (PostgreSQL, 2024-2025):

ID TypeOps/SecondLatencyIndex Size
ULID (bytea)~34,00058 μsBaseline
UUID v7~34,00058 μsSimilar
UUID v4~25,00085 μs85% larger

Key findings:

  • ULID performance comparable to or slightly better than UUID v7
  • 33% faster than UUID v4
  • Significantly more stable performance (lower variance)

Lexicographic Sorting Benefits

Chronological ordering:

  • ULIDs sort lexicographically in timestamp order
  • No need for additional timestamp indexes
  • Natural time-based ordering

Query optimization benefits:

-- Time-range queries are efficient
SELECT * FROM events
WHERE event_id >= '01ARZ3NDEK000000000000000'
  AND event_id <= '01ARZ3NDEKZZZZZZZZZZZZZZ';

Advantages:

  • Efficient range queries on time-based data
  • Simplified debugging (IDs reveal creation time)
  • Better query planner optimization
  • Natural partitioning by time ranges

Impact on Page Splits and Fragmentation

Dramatically reduced fragmentation compared to UUID v4:

UUID v4 problems:

  • Excessive page splits even before pages are full
  • Random writes throughout B-tree structure
  • Index bloat increases size on disk
  • Temporally related rows spread across index

ULID advantages:

  • Inserts at end of B-tree
  • Minimizes splits to only last page
  • Sequential writes optimize for append-heavy workloads
  • Reduced index maintenance overhead

Storage efficiency:

  • Less wasted space from partial pages
  • More compact indexes
  • Better compression ratios
  • Lower storage costs for write-heavy applications

Sequential Nature and Timestamp Ordering

48-bit timestamp component:

  • Millisecond precision Unix timestamp
  • Representation until year 10889 AD
  • High-order bits ensure chronological insertion
  • Enables time-based partitioning strategies

Performance characteristics:

  • New records naturally fall at end of B-tree
  • Predictable insertion patterns
  • Optimizes for sequential writes
  • Reduces fragmentation over time

Generation Approach

✅ Fully Decentralized

ULIDs can be generated in a completely decentralized manner with no coordination required.

No centralized service needed:

  • Each system/node generates independently
  • Only requires system clock access
  • Cryptographically secure random number generator (CSPRNG)
  • No network coordination overhead

Structure: Timestamp + Randomness

128 bits total:

 01AN4Z07BY      79KA1307SR9X4MV3
|----------|    |----------------|
 Timestamp          Randomness
   48bits             80bits

Timestamp component (48 bits):

  • Milliseconds since Unix epoch
  • First 10 characters in encoded form
  • Provides temporal ordering

Randomness component (80 bits):

  • Cryptographically secure random value
  • Remaining 16 characters
  • Ensures uniqueness within same millisecond

Binary encoding:

  • Most Significant Byte first (network byte order)
  • Each component encoded as octets
  • Total: 16 octets (bytes)

Collision Resistance

Extremely high collision resistance:

  • 1.21 × 10²⁴ unique IDs per millisecond (2⁸⁰ possible values)
  • Collision probability is practically zero
  • Even in distributed systems, likelihood of collision is exceedingly low

Example scale:

  • Would need to generate trillions of IDs per millisecond to see collisions
  • Far exceeds any practical generation rate
  • Safe for production at any realistic scale

Monotonicity Guarantees

Standard Generation (Non-Monotonic)

Default behavior:

  • Each ULID uses fresh random 80 bits
  • Sortable by timestamp (millisecond precision)
  • No guarantee of order within same millisecond

Monotonic Mode (Optional)

Algorithm:

  1. If timestamp same as previous: increment previous random component
  2. If timestamp advanced: generate fresh random component
  3. If overflow (2⁸⁰ increments): wait for next millisecond or fail

Benefits:

  • ✅ Guarantees strict ordering even at sub-millisecond generation
  • ✅ Better collision resistance through sequential randomness
  • ✅ Maintains sortability within same timestamp

Trade-offs:

  • ⚠️ Leaks information about IDs generated within same millisecond
  • ⚠️ Potential security concern: enables enumeration attacks
  • ⚠️ Can overflow if > 2⁸⁰ IDs generated in one millisecond (theoretical only)

Collision probability in monotonic mode:

  • Actually reduces collision risk
  • Incrementing creates number groups less likely to collide
  • Safe to use in production systems

Comparison to UUID v7

Both ULID and UUID v7 solve similar problems with different approaches:

AspectULIDUUID v7
Size16 bytes16 bytes
Timestamp bits4848
Random bits8074
String format26 chars (Base32)36 chars (hex + hyphens)
StandardizationCommunity specRFC 9562 (official)
DB supportCustomNative (PostgreSQL 18+)
ReadabilityBetter (Base32)Standard (hex)
Case sensitivityInsensitiveInsensitive
HyphensNone4 hyphens

ULID advantages:

  • More compact string representation (26 vs 36)
  • Slightly more random bits (80 vs 74)
  • Better human readability (Crockford Base32)
  • No hyphens (simpler to handle)

UUID v7 advantages:

  • Official RFC standardization
  • Growing native database support
  • URN namespace compatibility (urn:uuid:...)
  • Wider vendor tooling support

2024-2025 Landscape

Current state:

  • UUID v7 (RFC 9562, 2024) now offers similar benefits with standardization
  • ULID remains compelling for human readability and compact representation
  • Both vastly superior to UUID v4 for database performance
  • Choice often: standardization (v7) vs. readability (ULID)

Industry adoption:

  • incident.io uses ULIDs for all identifiers
  • Various startups prefer ULID for API design
  • UUID v7 gaining traction in enterprise systems

Use Cases

ULIDs are excellent for:

  • ✅ Database primary keys (especially write-heavy workloads)
  • ✅ Distributed systems requiring decentralized ID generation
  • ✅ Applications needing URI-safe identifiers
  • ✅ Systems benefiting from time-ordered IDs
  • ✅ Scenarios requiring human-readable identifiers
  • ✅ APIs where compact IDs are valued

Consider alternatives when:

  • ⚠️ Strict RFC/ISO standardization required (use UUID v7)
  • ⚠️ Native database support is priority (UUID v7 has better tooling)
  • ⚠️ Absolute minimal storage (auto-increment or Snowflake)
  • ⚠️ High-security scenarios sensitive to timing information leakage

Implementation Examples

PostgreSQL

-- Store as bytea for optimal performance
CREATE TABLE events (
    event_id BYTEA PRIMARY KEY DEFAULT ulid_generate(),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    data JSONB
);

-- Custom function needed (no native support)
CREATE OR REPLACE FUNCTION ulid_generate()
RETURNS BYTEA AS $$
    -- Implementation using pgcrypto or external library
$$ LANGUAGE plpgsql;

MySQL

-- Store as BINARY(16)
CREATE TABLE events (
    event_id BINARY(16) PRIMARY KEY,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    data JSON
);

-- Generate in application layer

Application-Level Generation

Go example:

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

// Standard generation
id := ulid.Make()
fmt.Println(id.String()) // 01ARZ3NDEKTSV4RRFFQ69G5FAV

// Monotonic generation
entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0)
id := ulid.MustNew(ulid.Timestamp(time.Now()), entropy)

Go Library Support

✅ oklog/ulid Library

The canonical Go library for ULIDs is github.com/oklog/ulid/v2, which provides full ULID specification support with both standard and monotonic generation modes.

Installation:

go get github.com/oklog/ulid/v2

Usage examples:

import (
    "crypto/rand"
    "github.com/oklog/ulid/v2"
)

// Simple generation with default entropy
id := ulid.Make()
fmt.Println(id.String()) // e.g., 01ARZ3NDEKTSV4RRFFQ69G5FAV

// Monotonic generation for strict ordering
entropy := ulid.Monotonic(rand.Reader, 0)
id := ulid.MustNew(ulid.Timestamp(time.Now()), entropy)

Summary

ULID represents an excellent choice for modern distributed systems:

Key strengths:

  1. Fully decentralized - no coordination required
  2. URI-safe and compact - 26 characters, no special chars
  3. Excellent database performance - time-ordered, minimal fragmentation
  4. Human-readable - Crockford Base32 alphabet
  5. High collision resistance - 1.21 × 10²⁴ IDs per millisecond

Key considerations:

  1. Not officially standardized (community spec)
  2. Requires custom database functions (no native support)
  3. Exposes creation timestamp (like UUID v7)
  4. Slightly more complex than UUID v4 generation

Bottom line: ULID is an excellent choice when you value compact, human-readable identifiers and don’t require strict RFC compliance. For official standardization, UUID v7 offers similar performance with growing vendor support.