Back to Blog

    UUID vs ULID — Which Should You Use?

    Choosing the right unique identifier strategy is one of the most impactful — and most overlooked — decisions in software architecture. Both UUID and ULID solve the problem of generating globally unique IDs without a central authority, but they do so in fundamentally different ways. This guide breaks down the differences in format, sortability, database indexing performance, and real-world applicability so you can make the right call for your project in 2025.

    What is a UUID?

    A UUID (Universally Unique Identifier) is a 128-bit label defined by RFC 4122 (and updated by RFC 9562) used to uniquely identify information in computer systems. UUIDs are standardized by ISO/IEC 9834-8 and are widely adopted across databases, APIs, operating systems, and distributed systems worldwide.

    The canonical UUID format is a 32-character hexadecimal string split into five groups separated by hyphens, following the pattern 8-4-4-4-12. This gives a total of 36 characters including the four hyphens. A typical UUID v4 looks like this:

    550e8400-e29b-41d4-a716-446655440000
    ^^^^^^^^ ^^^^ ^^^^ ^^^^ ^^^^^^^^^^^^
    8 chars  4    4    4    12 chars

    The 128 bits encode version and variant information in specific nibble positions. The high bits of the third group indicate the version (1–8), and the high bits of the fourth group indicate the variant (almost always 10xx for RFC 4122 UUIDs). This structure means any compliant UUID parser can immediately identify its version and variant without additional context.

    UUIDs are supported natively in PostgreSQL (UUID type), MySQL (CHAR(36) or binary storage), MongoDB (BinData UUID), and virtually every major programming language runtime. Their ubiquity makes them the default choice for unique IDs in most enterprise systems.

    What is a ULID?

    A ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit identifier designed to address the most glaring limitation of UUID v4: the complete lack of ordering. Introduced by Alizain Feerasta in 2016, ULIDs encode a 48-bit millisecond timestamp in the most significant bits, followed by 80 bits of cryptographic randomness.

    ULIDs are represented as 26 characters using Crockford Base32 encoding — an alphabet of 0123456789ABCDEFGHJKMNPQRSTVWXYZ — which excludes visually ambiguous characters like I, L, O, and U. A typical ULID looks like this:

    01HXYZ3KBCDEFGHJKMNPQRSTVW
    ^^^^^^^^^^ ^^^^^^^^^^^^^^^^
    10 chars   16 chars
    (timestamp)(randomness)

    Because the timestamp occupies the most significant bits, ULIDs are naturally lexicographically sortable. IDs generated later will always sort after IDs generated earlier — a property that has huge implications for database index performance and event log ordering.

    ULIDs also support a monotonicity guarantee: when multiple ULIDs are generated within the same millisecond, the random component is incremented by 1 to preserve sort order even within a single millisecond boundary.

    UUID Versions Explained

    The UUID specification defines multiple versions, each with a different generation strategy and use case. Understanding the differences is critical when evaluating uuid vs ulid trade-offs, because not all UUIDs are equal.

    VersionGeneration MethodSortable?Best Use Case
    v1Time + MAC addressPartiallyLegacy systems, Cassandra
    v3MD5 hash of namespace + nameNoDeterministic IDs from known names
    v4Random (CSPRNG)NoGeneral-purpose unique IDs, tokens
    v5SHA-1 hash of namespace + nameNoReproducible IDs, DNS-based namespaces
    v7Unix timestamp (ms) + randomYes ✓Modern DBs, distributed systems, logs

    UUID v4 is by far the most commonly used version today, due to its simplicity and strong collision resistance (122 bits of randomness). However, UUID v7 is rapidly gaining adoption as the recommended replacement because it adds time-ordering without sacrificing compatibility.

    How ULID Works — Timestamp, Randomness & Monotonicity

    The ULID specification defines a precise binary layout. The 128 bits are split as follows:

    • Bits 0–47 (48 bits): Unix time in milliseconds. This gives a range from January 1, 1970 to approximately year 10895 — far beyond any practical concern.
    • Bits 48–127 (80 bits): Cryptographically secure random data, providing 2^80 ≈ 1.2 × 10^24 possible values per millisecond per node.

    The monotonicity feature is what separates a naive timestamp-prefixed ID from a proper ULID implementation. When your application generates multiple ULIDs within the same millisecond (common in high-throughput systems), a compliant ULID library will increment the random suffix by 1 for each subsequent ID. This guarantees that even IDs generated in the same millisecond are strictly ordered:

    // Three ULIDs generated in the same millisecond
    01HXYZ3KBCDEFGHJKMNPQRST01   // first
    01HXYZ3KBCDEFGHJKMNPQRST02   // second (random incremented)
    01HXYZ3KBCDEFGHJKMNPQRST03   // third  (random incremented again)

    This monotonicity guarantee makes ULIDs ideal for event sourcing systems, append-only logs, and any architecture where insertion order must be recoverable from the ID itself — without querying a separatecreated_at column.

    UUID vs ULID: Side-by-Side Comparison

    Here is a comprehensive comparison of UUID v4, ULID, and UUID v7 across the dimensions that matter most for production systems:

    PropertyUUID v4ULIDUUID v7
    Format8-4-4-4-12 hex26-char Crockford base328-4-4-4-12 hex
    String Length36 chars (with hyphens)26 chars36 chars (with hyphens)
    Lexicographic Sort❌ No✅ Yes✅ Yes
    Monotonic❌ No✅ Yes (per-ms increment)⚠️ Implementation-dependent
    Timestamp Embedded❌ No✅ Yes (48-bit ms)✅ Yes (48-bit ms)
    URL-safe⚠️ With encoding✅ Yes (no special chars)⚠️ With encoding
    DB Index Friendliness❌ Poor (random scatter)✅ Excellent (sequential)✅ Excellent (sequential)
    Standardization✅ RFC 9562 / ISO⚠️ Community spec✅ RFC 9562
    Native DB Support✅ Most databases⚠️ As text/binary✅ Growing support
    Collision Resistance✅ 122 random bits✅ 80 random bits/ms✅ ~74 random bits
    ReadabilityLow (random hex)Medium (time prefix visible)Medium (time prefix visible)

    Database Indexing & Performance

    This is where the UUID vs ULID performance debate gets most consequential. The problem with UUID v4 as a primary key is well-documented and stems from a single root cause: randomness causes B-tree index fragmentation.

    Why UUID v4 Hurts Database Performance

    B-tree indexes work efficiently when new values are inserted in monotonically increasing order. When this happens, new leaf pages are always appended to the right-most edge of the tree — a pattern called sequential insert. With UUID v4, new IDs are randomly distributed across the entire keyspace. Every insert lands at a random position in the B-tree, causing:

    • Page splits: Existing index pages must be split to accommodate new random values, increasing I/O operations.
    • Cache thrashing: Random access patterns defeat the OS page cache — pages that were just evicted are needed again immediately.
    • Index bloat: Fragmented B-trees have lower fill factors, wasting disk space and increasing read amplification.
    • Write amplification: Each insert may trigger multiple page writes instead of one sequential append.

    A benchmark by Percona found that switching from UUID v4 to sequential IDs in MySQL reduced INSERT latency by up to 300% and index size by over 30% at scale. The effect worsens non-linearly as table size grows beyond the available buffer pool size.

    How ULID and UUID v7 Fix This

    Both ULID and UUID v7 embed a millisecond-precision timestamp in the most significant bits. This means new IDs are almost always larger than all existing IDs, enabling the efficient right-edge insert pattern in B-trees.

    -- PostgreSQL: Using UUID v7 as a primary key
    CREATE TABLE events (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- UUID v4 (BAD for perf)
      created_at TIMESTAMPTZ DEFAULT NOW()
    );
    
    -- Better: UUID v7 (sequential, no separate created_at needed)
    CREATE TABLE events (
      id UUID PRIMARY KEY DEFAULT uuidv7(), -- sequential inserts
      -- timestamp is embedded in the id itself!
    );
    
    -- Or ULID stored as TEXT or BYTEA
    CREATE TABLE events (
      id TEXT PRIMARY KEY, -- ULID: '01HXYZ3KBCDE...'
      payload JSONB
    );

    In PostgreSQL, you can use the community extension pg_uuidv7 to generate UUID v7 values natively. For ULID, libraries like ulid-postgres provide generation functions. In MySQL 8+, the UUID_TO_BIN(uuid, 1) function can reorder UUID v1 bits for sequential storage — and UUID v7 works natively without any tricks.

    When to Use UUID

    Despite the performance drawbacks of UUID v4, there are many scenarios where UUID remains the right choice:

    • Industry-standard interoperability: Every database, ORM, framework, and API ecosystem natively understands UUID. If you are building a public API or integrating with third-party systems, UUID is the path of least resistance.
    • Privacy-sensitive IDs: UUID v4 reveals nothing about when a record was created. For user IDs or order IDs exposed publicly, ULID or UUID v7 could leak timing information that enables enumeration attacks or reveals business metrics (e.g., how many orders were placed today).
    • No ordering requirement: If your application never needs to sort by ID or paginate using the ID as a cursor, the performance overhead of UUID v4 may be acceptable given its simplicity.
    • Existing ecosystem tooling: Frameworks like Ruby on Rails, Django, Laravel, and Spring Boot have mature UUID support out of the box. Switching to ULID may require custom types, validators, and serializers.
    • Deterministic IDs: UUID v5 (SHA-1 namespace hash) lets you generate the same ID from the same input deterministically — useful for deduplication, content-addressable storage, and idempotent API operations.
    • Small tables: At low record counts (under a few million rows), the B-tree fragmentation effect is negligible. UUID v4 is perfectly fine for most applications that never approach database scale.

    When to Use ULID

    ULID shines in specific architectural contexts where time-ordering and database performance are critical:

    • Time-sorted event logs: Event sourcing systems, audit trails, and activity feeds benefit enormously from IDs that sort chronologically. With ULID, your events are always in order without an additional created_at index.
    • Cursor-based pagination: Instead of WHERE created_at > ?, you can paginate with WHERE id > ? using the ULID directly as the cursor. This is both simpler and more efficient.
    • High-write distributed systems: Microservices generating thousands of events per second benefit from ULIDs' sequential inserts. Index fragmentation at scale can mean the difference between a database that keeps up and one that falls behind under load.
    • URL-safe IDs: ULIDs use only alphanumeric characters, making them safe for use in URLs, file names, environment variables, and command-line arguments without percent-encoding.
    • Embedded timestamp: ULID lets you extract the creation timestamp from the ID itself without a database query — useful for debugging, TTL calculations, and data expiry logic.
    • Short, readable IDs: At 26 characters vs 36 (UUID with hyphens), ULIDs are 10 characters shorter — meaningful in high-volume systems where IDs appear in logs, traces, and network payloads billions of times per day.
    // JavaScript: Generate a ULID
    import { ulid } from 'ulid';
    
    const eventId = ulid();
    // '01HXYZ3KBCDEFGHJKMNPQRSTVW'
    
    // Extract timestamp from ULID
    import { decodeTime } from 'ulid';
    const timestamp = decodeTime(eventId);
    // 1717286400000 (Unix ms timestamp)
    console.log(new Date(timestamp));
    // 2024-06-02T00:00:00.000Z

    UUID v7 — The Best of Both Worlds?

    UUID v7, standardized in RFC 9562 (April 2024), is increasingly the recommended choice for new systems. It combines the universal format compatibility of UUID with the time-ordered sequential nature of ULID. Here is what makes it compelling:

    • Standard format: UUID v7 uses the familiar xxxxxxxx-xxxx-7xxx-xxxx-xxxxxxxxxxxx format. Every UUID library, database column, and ORM that supports UUID will accept UUID v7 without any schema changes.
    • Timestamp-ordered: The first 48 bits are a Unix millisecond timestamp, making UUID v7 lexicographically sortable — exactly like ULID.
    • Drop-in replacement: You can replace UUID v4 generation with UUID v7 in your ORM without changing your schema, API contracts, or downstream consumers.
    • Growing native support: PostgreSQL 17 is expected to ship with built-in UUID v7 support. The pg_uuidv7 extension provides it today. MySQL, SQLite, and major ORMs are adding support rapidly.

    The main trade-off: UUID v7 has slightly fewer random bits (~74) than UUID v4 (122), which is still astronomically collision-resistant for any practical system. And unlike ULID, UUID v7 doesn't have an officially standardized monotonicity algorithm within a millisecond — though many implementations add this as an extension.

    For greenfield projects in 2025, UUID v7 is often the pragmatic winner: you get sequential inserts, embedded timestamps, and full UUID compatibility without adopting a non-standard format.

    Real-World Examples: UUID v4, ULID, and UUID v7 Side by Side

    Here are real-world example strings for each identifier type, along with JavaScript generation code:

    // Example IDs (generated June 2, 2025)
    
    // UUID v4 — fully random, no time ordering
    const uuidV4 = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
    
    // ULID — 10-char timestamp prefix + 16-char random (26 chars total)
    const ulidId = "01HWZM3KBCDEFGHJKMNPQRST01";
    
    // UUID v7 — time-ordered, standard UUID format
    const uuidV7 = "018f1a2b-3c4d-7e5f-8a9b-0c1d2e3f4a5b";
    //              ^^^^^^^^^^^^ timestamp portion (48 bits)
    //                           ^ version 7
    
    // Sorting demonstration
    const ids = [ulidId, ulid(), ulid()].sort();
    // Always chronological! ✓
    // Node.js generation examples
    
    // UUID v4 (built-in from Node 14.17+)
    import { randomUUID } from 'crypto';
    const v4 = randomUUID();
    // "f47ac10b-58cc-4372-a567-0e02b2c3d479"
    
    // UUID v7 (using uuid package v9+)
    import { v7 as uuidv7 } from 'uuid';
    const v7 = uuidv7();
    // "018f1a2b-3c4d-7000-8a9b-0c1d2e3f4a5b"
    
    // ULID (using ulid package)
    import { ulid } from 'ulid';
    const id = ulid();
    // "01HWZM3KBCDEFGHJKMNPQRST01"
    
    // ULID with custom timestamp (for testing/backfilling)
    const historical = ulid(new Date('2024-01-01').getTime());
    // "01HKZABC..."  ← starts with Jan 2024 timestamp

    Notice how the ULID and UUID v7 strings both start with a time-correlated prefix, while UUID v4 is entirely random. This is the core structural difference that drives all the performance and usability trade-offs described in this guide.

    FAQ: UUID vs ULID Common Questions

    Is ULID better than UUID?

    It depends on your use case. ULID is better for time-ordered, high-write scenarios where sequential database inserts and cursor pagination matter. UUID v4 is better when you need maximum interoperability, full randomness (no timestamp leakage), or you are integrating with systems that expect a standard UUID format. UUID v7 often offers a practical middle ground: sequential ordering in the standard UUID format. There is no universally "better" option — it depends on whether sort order, performance, privacy, or ecosystem compatibility is your top priority.

    Can I use ULID as a primary key in PostgreSQL?

    Yes. You can store ULIDs in PostgreSQL as TEXT, CHAR(26), or as BYTEA (binary, 16 bytes). The TEXT approach is most common and easiest to debug. For maximum indexing performance, store the ULID as BYTEA (raw 128-bit binary) since it reduces index size compared to storing the 26-character string. The ulid-postgres community extension provides generation and conversion functions. Alternatively, switching to UUID v7 stored in the native UUID column type gives you the same sequential benefits with better tooling support.

    Is UUID v4 truly random? Could there be collisions?

    UUID v4 uses 122 bits of cryptographically secure randomness (6 bits are fixed for version/variant). The probability of a collision is so astronomically low that it is effectively impossible in any real-world system. To have a 50% chance of a collision, you would need to generate approximately 2.7 × 10^18 UUIDs — that's 2.7 quintillion. At a rate of one billion UUIDs per second, this would take about 85 years. In practice, UUID v4 collision resistance is not a concern. The quality of the random number generator is more important than theoretical probability — always use a CSPRNG (Cryptographically Secure Pseudo-Random Number Generator), not Math.random().

    Are ULIDs URL-safe?

    Yes, ULIDs are fully URL-safe. The Crockford Base32 alphabet used by ULID contains only uppercase alphanumeric characters (0-9A-Z, excluding I, L, O, and U). There are no hyphens, plus signs, slashes, or equals signs — all characters that require percent-encoding in URLs. This makes ULIDs safe to use directly in URL path segments, query parameters, HTTP headers, file names, and environment variable values without any encoding or transformation. UUID v4, by contrast, contains hyphens which are technically URL-safe but can cause issues in some routing frameworks and file systems.

    Conclusion

    The UUID vs ULID debate is not about one being universally superior — it is about understanding the trade-offs and matching your ID strategy to your system's requirements.

    Here is a quick summary of our recommendations:

    • Use UUID v4 when you need maximum ecosystem compatibility, full randomness, and your table sizes are manageable (under tens of millions of rows).
    • Use ULID when you need URL-safe sortable IDs, are building event-driven or log-heavy systems, or want the embedded timestamp for debugging and TTL logic.
    • Use UUID v7 when you want sequential inserts (like ULID) but need to stay within the standard UUID format for maximum database and ORM compatibility. This is the recommended choice for most new projects in 2025.

    Whichever format you choose, avoid auto-incrementing integer primary keys for distributed systems — they create coordination bottlenecks and expose enumeration vulnerabilities. And if you are still using UUID v4 in a high-write database that is showing index performance degradation, migrating to UUID v7 may be the single most impactful optimization you can make without touching your application logic.

    Ready to generate UUIDs instantly? Use our free online tool to create UUID v1, v3, v4, v5, and v7 values in bulk — no signup required, runs entirely in your browser.

    → Try the Free UUID Generator Tool