UUIDv7 vs ULID: Which Time-Sortable ID Should You Use?

    Comparing two modern approaches to generating sortable, globally unique identifiers

    TL;DR

    • Both are 128-bit, time-sortable identifiers with a 48-bit millisecond timestamp + 80 bits of randomness
    • UUIDv7 is an IETF standard (RFC 9562), uses hex-with-dashes format, and fits natively into database UUID columns
    • ULID is a community spec, uses Crockford's Base32 (26 chars vs 36), and is case-insensitive
    • Database support: UUIDv7 wins -- native UUID types in PostgreSQL, MySQL, etc. vs ULID needing CHAR(26) or BINARY(16)
    • Ecosystem: UUIDv7 is compatible with the massive UUID tooling ecosystem; ULID has its own smaller ecosystem
    • Choose UUIDv7 for new projects; choose ULID if you need shorter strings or already use it

    Both UUIDv7 and ULID solve the same problem: generating globally unique, time-sortable identifiers without coordination. They share remarkably similar designs -- a 48-bit millisecond timestamp followed by random bits, all packed into 128 bits. So why do both exist, and which should you choose? The answer comes down to standardization, encoding, and ecosystem support.

    At a Glance

    UUIDv7 (RFC 9562)

    01936b2e-1e85-7000-8000-4a6f7e3b2c1d

    48-bit timestamp + version/variant bits + 62 random bits. IETF standard. Hex encoding with dashes. 36 characters.

    ULID (Community Spec)

    01ARZ3NDEKTSV4RRFFQ69G5FAV

    48-bit timestamp + 80 random bits. Community specification. Crockford's Base32. 26 characters.

    Did you know?Millisecond timestamps have 13 digitsLearn more

    Detailed Comparison

    FeatureUUIDv7ULID
    StandardIETF RFC 9562 (2024)Community specification
    FormatHex with dashes (8-4-4-4-12)Crockford's Base32
    String length36 characters26 characters
    Binary size128 bits (16 bytes)128 bits (16 bytes)
    Timestamp bits48 bits (ms precision)48 bits (ms precision)
    Random bits62 bits (6 used for ver/var)80 bits
    SortabilityLexicographic (string & binary)Lexicographic (string & binary)
    Native DB typeUUID (all major databases)None (use CHAR/BINARY)
    Case sensitivityCase-insensitive (hex)Case-insensitive (Base32)
    EcosystemMassive (UUID compatible)Growing but smaller

    Format Deep Dive

    Despite their different string representations, UUIDv7 and ULID have remarkably similar internal structures. Both dedicate their first 48 bits to a Unix millisecond timestamp, followed by random data.

    UUIDv7 Layout (128 bits)

    unix_ts_ms (48 bits)ver (4)rand_a (12)var (2)rand_b (62)

    6 bits used for version (0111) and variant (10) markers, leaving 74 bits of randomness

    ULID Layout (128 bits)

    unix_ts_ms (48 bits)randomness (80 bits)

    No version/variant overhead -- all 80 remaining bits are random

    Timestamp
    Version
    Variant
    Random

    Key difference: UUIDv7 reserves 6 bits for version and variant markers (required by the UUID standard), which means 62 usable random bits per millisecond. ULID uses all 80 remaining bits for randomness. In practice, both provide far more than enough uniqueness -- 62 random bits means over 4.6 quintillion possible values per millisecond.

    Did you know?UUIDv7 outperforms UUIDv4 for database primary keysLearn more

    Ecosystem Support

    One of the most significant practical differences is ecosystem compatibility. UUIDv7 benefits from decades of UUID infrastructure.

    UUIDv7 Ecosystem

    • +Native UUID column type in PostgreSQL, MySQL, SQL Server, etc.
    • +PostgreSQL 17+ native generation (gen_random_uuid_v7)
    • +Compatible with every UUID library, ORM, and API
    • +Supported in Go (google/uuid), Java (java.util.UUID), .NET, and more
    • +Standard UUID validation and parsing works unchanged

    ULID Ecosystem

    • +Libraries in JavaScript, Python, Go, Java, Ruby, Rust, and more
    • +Shorter string representation (26 vs 36 characters)
    • xNo native database column type
    • xRequires custom validation logic
    • xNot recognized by standard UUID-aware tools and APIs

    When to Use Each

    Choose UUIDv7 When...

    • Database primary keys -- native UUID column type for efficient storage
    • Interoperability -- APIs, microservices, or systems that already use UUIDs
    • Standards compliance -- IETF RFC 9562 gives long-term stability
    • New projects -- modern default with the strongest ecosystem support
    • PostgreSQL 17+ -- take advantage of native gen_random_uuid_v7()
    • ORM compatibility -- every ORM supports UUID fields out of the box

    Choose ULID When...

    • String length matters -- 26 chars vs 36 for URLs, logs, or display
    • Existing ULID usage -- already adopted in your stack with no issues
    • No UUID column available -- using a datastore without native UUID support
    • Maximum randomness -- 80 random bits vs 62 (rarely a practical concern)
    • URL-friendly IDs -- no dashes, case-insensitive, shorter representation

    Code Examples

    JavaScript / Node.js
    // UUIDv7 — npm install uuidv7
    import { uuidv7 } from "uuidv7";
    const id1 = uuidv7();
    // "01936b2e-1e85-7000-8000-4a6f7e3b2c1d"
    
    // ULID — npm install ulid
    import { ulid } from "ulid";
    const id2 = ulid();
    // "01ARZ3NDEKTSV4RRFFQ69G5FAV"
    
    // Extract timestamp from either:
    // UUIDv7: first 12 hex chars (48 bits)
    const tsV7 = parseInt(
      id1.replace(/-/g, "").slice(0, 12), 16
    );
    
    // ULID: first 10 Base32 chars (48 bits)
    const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
    let tsUlid = 0;
    for (const c of id2.slice(0, 10)) {
      tsUlid = tsUlid * 32 + CROCKFORD.indexOf(c);
    }
    Python
    # UUIDv7 — pip install uuid7
    from uuid_extensions import uuid7
    id_v7 = uuid7()
    # UUID('01936b2e-1e85-7000-8000-4a6f7e3b2c1d')
    
    # ULID — pip install python-ulid
    from ulid import ULID
    id_ulid = ULID()
    # ULID(01ARZ3NDEKTSV4RRFFQ69G5FAV)
    
    # Both provide timestamp extraction:
    print(id_ulid.timestamp())  # float epoch
    # UUIDv7 timestamp:
    ts = int(str(id_v7).replace('-', '')[:12], 16)
    PostgreSQL
    -- UUIDv7: native UUID column, native generation (PG 17+)
    CREATE TABLE events (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid_v7(),
      name TEXT NOT NULL
    );
    
    -- ULID: no native type — use CHAR(26) or BINARY
    CREATE TABLE events_ulid (
      id CHAR(26) PRIMARY KEY,  -- or BYTEA for binary
      name TEXT NOT NULL
    );
    -- Must generate ULID in application code
    -- No native ULID functions in PostgreSQL
    Go
    // UUIDv7 — github.com/google/uuid
    import "github.com/google/uuid"
    id, _ := uuid.NewV7()
    // 01936b2e-1e85-7000-8000-4a6f7e3b2c1d
    
    // ULID — github.com/oklog/ulid/v2
    import "github.com/oklog/ulid/v2"
    id := ulid.Make()
    // 01ARZ3NDEKTSV4RRFFQ69G5FAV
    
    // Both sort correctly as strings
    // UUIDv7 also sorts correctly in binary (UUID type)
    Did you know?ISO 8601 is the international standard for date formatsLearn more

    Frequently Asked Questions

    Try Our UUIDv7 Tools

    Generate UUIDv7 identifiers or extract timestamps from existing UUIDs.

    Related Articles

    We use cookies to analyze traffic and improve your experience. Learn more