UseToolSuite UseToolSuite

UUID vs NanoID vs ULID: Picking the Right ID for Your Project

A definitive architectural guide to database primary keys. Learn why UUID v4 destroys B-Tree indexing performance, the mathematical collision risks of NanoID, and why time-sorted ULIDs (and UUIDv7) are the future of backend infrastructure.

Necmeddin Cunedioglu Necmeddin Cunedioglu 7 min read
Part of the Developer Generators: The Tools That Save You Hours Every Week series

Practice what you learn

UUID Generator

Try it free →

UUID vs NanoID vs ULID: Picking the Right ID for Your Project

Database primary keys are easy to treat as an afterthought. You spin up a new PostgreSQL or MySQL table, map uuid_generate_v4() to the primary key, feel good about not using predictable auto-incrementing integers (which leak business data to competitors), and move on.

It works in local development, in staging, and in production for the first year. Then the users table crosses 10 million rows, and INSERT operations that used to take 2ms start taking 45ms. Database CPU spikes. You start seeing “index page splits” and “write amplification” in the monitoring.

That’s the trap of the random identifier.

If you’re generating IDs for API tokens, database primary keys, distributed storage, or public URL slugs, the choice between UUID v4, NanoID, and ULID isn’t aesthetic. It affects the scalability, hardware cost, and performance ceiling of your backend.

Stop writing boilerplate ID generation functions. Inspect the string formats, benchmark the cryptographic entropy, and generate bulk identifiers instantly using our local UUID & ULID Generator.

1. The Three Contenders at an Architectural Glance

Feature SpecificationUUID v4NanoIDULID
String Format550e8400-e29b...V1StGXR8_Z5jd...01ARZ3NDEKTSV...
Absolute Length36 characters21 characters26 characters
Encoding AlphabetHexadecimal (0-9, a-f)URL-Safe (A-Z, a-z, 0-9, -, _)Base32 (Excludes I, L, O, U)
Total Entropy122 bits126 bits80 bits of true randomness
Time-Sorted (Chronological)No (Completely Random)No (Completely Random)Yes (48-bit millisecond timestamp)
URL & HTTP SafeNo (Hyphens require encoding)YesYes
B-Tree Indexing SpeedCatastrophic at scaleCatastrophic at scaleFlawless (O(1) appends)

2. The Legacy Industry Default: UUID Version 4

A UUID Version 4 is generated from cryptographic random numbers. It’s the industry standard (RFC 4122) and is supported natively by almost every language, runtime, and database.

import { v4 as uuidv4 } from 'uuid';
const secureId = uuidv4(); 
// Output: "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"

The problem: B-Tree index fragmentation

Relational databases (PostgreSQL, MySQL, Oracle) use B-Tree (balanced tree) structures to keep indexes in RAM. A B-Tree is optimized for sequential, ordered data (1, 2, 3 or 2024-01, 2024-02).

When you insert sequential data, the engine appends the new record to the rightmost edge of the tree. The data pages stay packed in RAM, and disk writes stay minimal.

UUID v4 is fully random. a1b2... might be generated today and 00f4... tomorrow.

When you insert a random UUID v4 into a B-Tree primary key index, the database has to find where that random string belongs in sorted order, then split an existing page in half to make room. That’s an index page split.

At 100,000 rows there’s enough RAM to absorb this. At 10 million rows, the cache is exhausted, the database starts reading from disk constantly, splitting pages and thrashing the cache. When a single insert triggers multiple expensive disk operations, that’s write amplification, and it kills performance.

Mitigating UUIDs

If you must use UUIDs in PostgreSQL, don’t store them as text in a VARCHAR(36) column — strings take 37+ bytes and use slow string comparison. Use the native UUID column type. PostgreSQL strips the dashes and stores it as 16-byte binary, which speeds up comparisons (though it doesn’t solve the random-insertion problem).

3. The Lightweight Frontend Challenger: NanoID

NanoID was engineered specifically by frontend developers to fix the bloated, non-URL-safe nature of traditional UUIDs. It packs an incredible 126 bits of cryptographic entropy into just 21 characters by utilizing a much larger, denser alphabet (a Base64 URL-safe alphabet, including uppercase, lowercase, numbers, hyphens, and underscores).

import { nanoid } from 'nanoid';
const token = nanoid(); 
// Output: "V1StGXR8_Z5jdHi6B-myT"

Where NanoID Dominates the Industry

Because it is incredibly short and completely URL-safe without requiring percent-encoding, NanoID is the absolute king of client-facing, public identifiers.

  • Short URLs and Application Slugs: https://myapp.com/docs/xK9f2mP
  • Session Tokens: Lightweight tokens transmitted continuously in HTTP headers.
  • React/Vue/Svelte Key Props: Generating highly performant, unique keys for DOM list mapping operations in the browser.

The catch: the birthday paradox

NanoID lets you customize the output length, and it’s tempting to drop it to 8 characters for readability: nanoid(8).

Don’t do this for database primary keys. An 8-character NanoID has roughly 48 bits of entropy, and because of the birthday paradox, collisions happen far sooner than intuition suggests. At 48 bits, generating ~17 million IDs gives you about a 1% chance of a collision — and a primary-key collision crashes your INSERT. If uniqueness is critical, keep NanoID at its default 21 characters or longer.

4. The Future of Distributed Databases: ULID

ULID (Universally Unique Lexicographically Sortable Identifier) was explicitly engineered by backend architects to solve the database index fragmentation crisis caused by UUIDs.

A ULID is precisely 26 characters long, utilizing Crockford’s Base32 alphabet. (It explicitly removes the letters I, L, O, and U to prevent visual ambiguity for human users and prevent the accidental generation of profanity).

 01ARZ3NDEKTSV4RRFFQ69G5FAV
|----------|----------------|
 Timestamp    Randomness
 (48 bits)    (80 bits)

The Architectural Magic of ULID

The first 10 characters of a ULID encode the exact Unix Timestamp (in milliseconds) of when the ID was generated. The remaining 16 characters are cryptographically hardware random.

Because the timestamp sits at the front of the string, ULIDs sort chronologically by default.

A ULID generated on Tuesday sorts after one from Monday. Insert millions of ULIDs into PostgreSQL and the B-Tree index stays healthy — the engine appends each record to the rightmost edge. No page splits, no cache thrashing. Insert performance stays flat and predictable whether the table has 10,000 rows or 10 billion.

You also get chronological sorting for free. ORDER BY id DESC returns the same result as ORDER BY created_at DESC, so you don’t need a secondary index on the timestamp column.

Extracting Free Data from ULIDs

Because the timestamp is permanently baked into the ID string, you can extract it natively on the backend without ever querying the database:

import { decodeTime } from 'ulid';

const databaseId = "01ARZ3NDEKTSV4RRFFQ69G5FAV";
const creationTimeMs = decodeTime(databaseId);
const parsedDate = new Date(creationTimeMs); 
// Console output: 2016-07-30T23:56:16.385Z

The Monotonicity Trap (Extreme Scale)

A serious architectural edge-case exists. What happens if your server generates 5,000 unique ULIDs within the exact same specific millisecond?

Because the timestamp component is identical for all 5,000 IDs, the sorting relies entirely on the secondary random component. The IDs will scramble, permanently losing their chronological perfection within that specific 1-millisecond window.

To fix this, high-performance distributed systems utilize a Monotonic Factory. If a ULID is generated within the same millisecond as the previous one, the factory freezes the random component and simply increments the final mathematical bit by 1.

import { monotonicFactory } from 'ulid';
const generateMonotonic = monotonicFactory();

// Generated within the EXACT same 1ms window:
console.log(generateMonotonic()); // 01BX5ZZKBKACTAV9WEVGEMMVRY
console.log(generateMonotonic()); // 01BX5ZZKBKACTAV9WEVGEMMVRZ (Incremented)
console.log(generateMonotonic()); // 01BX5ZZKBKACTAV9WEVGEMMVS0 (Incremented)

This keeps IDs sortable even under heavy load.

5. UUID Version 7

In mid-2024, the IETF ratified an update to the UUID specification: UUID v7.

UUID v7 does what ULID pioneered — it prefixes a Unix timestamp to random data for chronological sortability and B-Tree index health — but keeps the traditional 36-character hyphenated UUID format (018e6a...-....).

If your database supports UUID v7 (PostgreSQL 17+ and many ORMs do), it’s worth adopting. You get ULID’s performance benefits while using the standard native UUID binary column type.

6. The Decision Matrix

A practical framework for choosing an identifier format:

Use Case ScenarioThe Architectural ChoiceThe Engineering Rationale
Relational DB Primary KeysULID (or UUID v7)Absolute necessity to prevent B-Tree index fragmentation, page splits, and catastrophic write amplification.
API Tokens & Session KeysNanoID (32+ chars)Must be highly entropic, incredibly secure, and perfectly URL-safe for transmission via HTTP Authorization headers. Chronological sorting is completely irrelevant here.
Public URL SlugsNanoID (11 chars)Needs to be extremely short, aesthetic, and URL-safe (the YouTube video ID style). Absolute collisions are mitigated at the application logic layer via database lookups.
E-Commerce Order NumbersUUID v4ULIDs explicitly leak the exact millisecond of creation. Competitors can scrape public ULID order numbers to precisely calculate your exact daily sales volume. Never expose timestamped IDs publicly if volume is a corporate trade secret.
Legacy Enterprise IntegrationUUID v4When interacting with legacy Java Spring or C# .NET systems that aggressively throw hard 500 exceptions if an ID string does not contain exactly four hyphens.

Further Reading


Stop writing fragile, custom ID logic. Inspect the entropy mathematics, test the format output, and generate bulk identifiers flawlessly using our local UUID & ULID Generator. Secure your custom session tokens natively with the Password Generator.

Necmeddin Cunedioglu
Necmeddin Cunedioglu Author
7 min read
-- views

Software developer and the creator of UseToolSuite. I write about the tools and techniques I use daily as a developer — practical guides based on real experience, not theory.