UUID vs ULID vs NanoID

In-Depth Technical Comparison & Architecture Guide

Generating unique identifiers is a foundational task in software development, whether you are seeding database primary keys, generating session tokens, or routing API endpoints. For decades, the standard choice has been UUIDs, particularly UUID v4. However, modern application demands have highlighted two notable limitations of UUID v4: random indexes fragment B-Tree structures in databases, and the 36-character length is verbose for client-facing URLs. This deep dive compares standard UUIDs, lexicographically sortable ULIDs, and lightweight, customizable NanoIDs across performance, collision risk, and indexing efficiency.

Quick Reference Matrix

MetricUUID v4ULIDNanoID (Default)
Size (Bits)128 bits128 bitsVariable (~126 bits default)
String Length36 characters (including hyphens)26 characters21 characters (customizable)
AlphabetHexadecimal (0-9, a-f)Base32 Crockford (alphanumeric)URL-safe Base64 (customizable)
SortabilityNo (completely random)Yes (lexicographically sortable)No (completely random)
Ambiguous CharsYes (contains 0, 1, O, L in strings)No (crockford filters out ambiguity)Yes (can be filtered via custom alphabet)
Database CompatibilityNative binary uuid columnsBinary compatible (fits 16 bytes)Text/String storage only
Timestamp PrecisionNoneMillisecondNone
Ideal ContextLegacy systems, database-level UUID columnsHigh-throughput relational database keysURLs, client-facing IDs, short tokens

Technology Overview

UUID (Universally Unique Identifier) is defined by RFC 4122 and provides 128-bit identifiers, standardizing on a 36-character string representation composed of hexadecimal blocks separated by hyphens. The most common variant, UUID v4, is generated entirely from pseudo-random or cryptographically secure random bits, offering simple collision safety but zero ordering.

ULID (Universally Unique Lexicographically Sortable Identifier) is designed to solve UUID v4's lack of sorting. It also packs 128 bits, but splits them into a 48-bit timestamp prefix (in milliseconds) and an 80-bit random suffix. Instead of hex, it uses Crockford's Base32 alphabet, resulting in a compact 26-character alphanumeric string. Because it starts with a timestamp, ULIDs sort chronologically, which prevents database index bloat during high-write operations.

NanoID is a highly flexible, lightweight alternative that doesn't constrain itself to 128 bits. It utilizes cryptographically secure random numbers to generate URL-safe alphanumeric strings. Developers can fully customize both the character alphabet and the identifier length, optimizing database storage and slug readability without sacrificing security.

Binary Structure, Alphabet Size, and Representation

UUIDs are strictly 128-bit structures. A standard UUID string looks like `f47ac10b-58cc-4372-a567-0e02b2c3d479`. It is composed of 32 hexadecimal characters and 4 hyphens. In storage, UUIDs can be packed into a raw 16-byte binary column (e.g. `uuid` type in PostgreSQL) to minimize overhead. Out of the 128 bits, UUID v4 reserves 6 bits for metadata (variant and version), leaving 122 bits of true entropy.

ULIDs are also 128-bit values, meaning they are binary-compatible with UUID database columns. They are represented using Crockford's Base32 alphabet (`0123456789ABCDEFGHJKMNPQRSTVWXYZ`), which excludes ambiguous letters like I, L, O, and U to prevent human transcription errors. The first 48 bits store a UNIX millisecond timestamp, and the remaining 80 bits provide randomness. A ULID is serialized as a 26-character string, such as `01H7Y8WZ7E8B83M8N7X2H9J4K2`.

NanoID uses a default alphabet of 21 characters containing numbers, lowercase letters, uppercase letters, and hyphens/underscores (`_~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`). Unlike UUID and ULID, it does not use a binary standard; it is treated purely as a string. By default, NanoID is 21 characters long, which provides similar security guarantees to UUID v4, but it can be resized to shorter lengths (e.g. 10-12 chars) for compact public tokens.

// Comparing representations and generation in JavaScript/TypeScript
import { v4 as uuidv4 } from 'uuid';
import { ulid } from 'ulid';
import { nanoid } from 'nanoid';

console.log(uuidv4()); // "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" (36 chars, Hex + hyphens)
console.log(ulid());   // "01H7Y98W6E8X8D8C8K8V8B8M8N" (26 chars, Crockford's Base32)
console.log(nanoid()); // "V1StGXR8_Z5jdHi6B-kY2" (21 chars, URL-safe Base64)

Generation output and length comparisons of UUID, ULID, and NanoID.

Database Indexing, B-Trees, and Page Splits

When using a relational database like PostgreSQL or MySQL (InnoDB), primary keys are typically indexed using a B-Tree structure. B-Trees sort entries sequentially to enable rapid range searches and binary lookups. When you insert a row with an auto-incrementing integer or a sortable key, the database appends the new index leaf to the end of the tree, keeping operations fast and data pages tightly packed.

UUID v4 generates completely random values. If you use UUID v4 as a primary key, new rows must be inserted at random locations throughout the B-Tree index. This causes "page splits": the database is forced to split existing index blocks to insert the new random key, leading to massive disk I/O operations, memory overhead, and highly fragmented indexes. As the table grows beyond RAM cache limits, write throughput drops dramatically.

ULID completely mitigates this indexing performance drop. Because the first 48 bits contain a millisecond-precision timestamp, newly generated ULIDs are lexicographically sorted. When rows are inserted, the index is always appended at the end, eliminating random page splits. This allows developers to enjoy the benefits of distributed ID generation without incurring InnoDB performance penalties.

Entropy Calculations and Collision Probability Math

Let's look at the math behind collision risk. UUID v4 has 122 bits of random entropy. The probability of a collision is governed by the Birthday Paradox. To have a 50% chance of a collision, you would need to generate $2.3 \times 10^{18}$ UUID v4s. At a rate of 1 billion IDs generated per second, it would take roughly 73 years to reach this threshold.

ULID has 80 bits of random entropy per millisecond. While 80 bits is less than UUID's 122 bits, the entropy is scope-locked to a single millisecond. To collide, two ULIDs must be generated in the exact same millisecond AND share the exact same 80-bit random value. Furthermore, most ULID libraries generate monotonic random bits: if multiple ULIDs are generated in the same millisecond on the same thread, the random component is incremented by 1, guaranteeing uniqueness.

NanoID's default length of 21 characters with a 64-character alphabet offers $64^{21} \approx 1.4 \times 10^{38}$ possible combinations. This represents roughly 126 bits of entropy. If you reduce NanoID to 12 characters, the combinations decrease to $64^{12} \approx 4.7 \times 10^{21}$, which is still extremely secure for user-facing routes but carries a higher collision risk in massive distributed systems.

Entropy Comparison:
- UUID v4: 122 bits of random entropy
- ULID: 48-bit timestamp + 80 bits of random entropy per millisecond
- NanoID (default 21 chars): ~126 bits of random entropy
- NanoID (custom 12 chars): ~72 bits of random entropy

Summary of cryptographic entropy distribution between ID standards.

Generation Speed and Library Overhead

Generation speed can vary depending on the runtime. Standard UUID generation libraries in Node.js are fast because they rely on native cryptographic bindings (`crypto.randomUUID()`). However, parsing and converting UUIDs between string and binary representation adds overhead in JS apps.

ULIDs require calling the system clock to fetch the millisecond timestamp, followed by random number generation. While slightly more complex than purely random generation, ULID libraries are still capable of generating millions of IDs per second. NanoID is highly optimized for performance; it uses a custom batching algorithm to retrieve random bytes from the OS cryptographical library in chunks rather than single bytes, making it faster than many traditional JavaScript UUID libraries.

UUID v4 Advantages & Disadvantages

Advantages / Pros

  • RFC 4122 standard is recognized by database engines (PostgreSQL, SQL Server) with native column optimization.
  • Extremely low collision rate backed by 122 bits of pure cryptographic entropy.
  • Supported by native environment APIs, such as `crypto.randomUUID()` in modern browsers and Node.js.

Disadvantages / Cons

  • Random distribution fragments database B-Tree indexes, degrading high-volume write performance.
  • Verbose 36-character representation is bulky for client-facing URLs or logging structures.
  • Hex format can include easily confused characters (like 1, l, 0, o) when printed.

ULID Advantages & Disadvantages

Advantages / Pros

  • Lexicographically sortable timestamp prefix ensures efficient database indexing and sequential insertions.
  • Compact 26-character layout saves index space and is easier to read.
  • Crockford's Base32 alphabet prevents human transcription errors by avoiding ambiguous letters.
  • Binary format maps cleanly into standard 16-byte UUID columns, facilitating migrations.

Disadvantages / Cons

  • Slightly exposes generation time (leaks millisecond precision of creation) in the ID prefix.
  • Requires system clock synchronization; clock-drift or leap seconds could theoretically affect ordering.
  • Less widely integrated in database-level native generator defaults compared to UUID.

NanoID Advantages & Disadvantages

Advantages / Pros

  • Fully customizable alphabet and character length to match precise design requirements.
  • Ultra-compact serialized footprint makes it perfect for clean URL routing and URL slugs.
  • Highly optimized generation performance with batched cryptographically secure random bytes.
  • Zero dependencies and small bundle size footprint in frontend clients.

Disadvantages / Cons

  • Cannot be stored in optimized binary UUID columns natively without translating to string.
  • Reducing length drastically increases the probability of id collisions (Birthday Paradox).
  • Does not support chronological sorting, making it subject to B-Tree index fragmentation.

Real-World Use Cases

UUID v4

Distributed microservices tracing

Injecting correlation IDs into HTTP headers (e.g. `X-Request-ID`) to trace requests across stateless microservices.

PostgreSQL Primary Keys

Leveraging native database `uuid` types when chronological ordering is not required, or when using UUID generators at the schema layer.

ULID

Database Primary Keys in MySQL/PostgreSQL

Replacing standard UUID v4 with sortable ULIDs to maintain database indexing performance under high-write transaction workloads.

Ordered Event Logging

Generating message identifiers for audit logs, message queues, and streaming pipelines where chronological sequencing is important.

NanoID

Short URL Slugs

Generating short, URL-safe database keys for link shorteners or user profile URL routes (e.g. `/user/dK91jLz`).

Client-Side Session IDs

Creating unique tokens directly in frontend applications to track client sessions or cache namespaces locally.

Developer Recommendation

Choose ULID if you are building database-driven applications (especially with MySQL, PostgreSQL, or SQL Server) and want to use UUID-like keys. ULID prevents B-tree fragmentation and keeps writes fast while fitting directly into binary UUID columns.

Choose NanoID if you need clean, readable, short identifiers for URLs, API parameters, or frontend state trackers. You can adjust the length down to save storage space for low-cardinality records.

Choose UUID v4 only if you are working with legacy enterprise systems that require strict RFC 4122 compliance, or when using platforms that natively support UUID generation inside the database layer.

Frequently Asked Questions

Are ULIDs binary-compatible with UUIDs?
Yes. Because both ULIDs and UUIDs are 128-bit values, you can store a ULID string in a database column typed as UUID. The database will parse the bits exactly the same way, keeping your index storage efficient.
Can someone predict a ULID because it has a timestamp?
The first 48 bits of a ULID represent the millisecond time, which is public knowledge. However, the remaining 80 bits are generated using cryptographically secure random entropy. Predicting the random part of a ULID is practically impossible, making it secure against ID harvesting.
What is the default alphabet of NanoID?
NanoID's default alphabet contains 21 characters, including alphanumeric symbols and URL-safe characters (`_` and `-`). It avoids character clashes in URLs and is highly compressed.
How do I resolve clock drift issues with ULIDs?
If the system clock drifts backwards, most ULID libraries handle it by either incrementing the random component of the last generated ID or rejecting generation until the clock catches up, ensuring monotonic sortability.
Is NanoID secure against brute-force attacks?
Yes. When using the default length of 21 characters, NanoID has 126 bits of entropy. It would take billions of years of computation to guess a specific ID, which is the same level of security as UUID v4.
Does ScriptPulse have generator tools for these IDs?
Yes. You can generate IDs instantly using the UUID Generator, ULID Generator, and Nano ID Generator tools on ScriptPulse.tools. All generation happens locally in your browser.