NC Logo UseToolSuite
Developer Tools

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

A practical comparison of UUID, NanoID, and ULID with common implementation mistakes and performance considerations for developers.

Necmeddin Cunedioglu Necmeddin Cunedioglu

Practice what you learn

UUID Generator

Try it free →

I used to slap uuid() on everything and call it a day. Primary keys, API tokens, file names, session IDs — all UUID v4. It worked, but I was unknowingly creating performance problems that didn’t surface until the database grew.

If you’re generating IDs in your application, the choice between UUID, NanoID, and ULID actually matters more than you’d think. Here’s what I’ve learned.

The Three Contenders

FeatureUUID v4NanoIDULID
Format550e8400-e29b-41d4-a716-446655440000V1StGXR8_Z5jdHi6B-myT01ARZ3NDEKTSV4RRFFQ69G5FAV
Length36 chars (with hyphens)21 chars (default)26 chars
SortableNoNoYes (timestamp-based)
Collision risk~1 in 2^122~1 in 2^126 (default)~1 in 2^80 per millisecond
URL-safeNo (hyphens)YesYes
Database index performancePoor (random)Poor (random)Good (sequential)
Spec/StandardRFC 4122No formal specFormal spec
DependenciesBuilt-in (most languages)npm package (tiny)npm package

Why UUID v4 Isn’t Always the Answer

UUID v4 is the default choice for most developers, and for good reason — it’s built into practically every language and framework. But it has two significant drawbacks that only show up at scale.

Problem 1: Random UUIDs Destroy Index Performance

When you use UUID v4 as a primary key in a B-tree indexed database (PostgreSQL, MySQL, SQLite), every new insert lands at a random position in the index. This causes:

  • Page splits — the database constantly reorganizes index pages
  • Cache misses — recently written rows aren’t near each other in memory
  • Write amplification — more disk I/O per insert than necessary

I noticed this firsthand when a table hit around 5 million rows. Insert times had gradually increased by about 40%, and the database was doing significantly more I/O than expected. Switching the primary key to ULID (which is time-sorted) brought insert performance back to normal.

Problem 2: UUIDs Are Wasteful in URLs

https://api.example.com/users/550e8400-e29b-41d4-a716-446655440000

That’s 36 characters of ID in your URL. Compare with NanoID:

https://api.example.com/users/V1StGXR8_Z5jdHi6B-myT

21 characters, URL-safe by default, no hyphens to worry about. In high-traffic APIs, those extra bytes add up across millions of requests.

NanoID: When Size and Speed Matter

NanoID was designed to be a smaller, faster, URL-friendly alternative to UUID. It’s 126 bits of randomness packed into 21 characters using a URL-safe alphabet (A-Za-z0-9_-).

import { nanoid } from 'nanoid';

const id = nanoid();       // "V1StGXR8_Z5jdHi6B-myT"
const short = nanoid(10);  // "IRFa-VaY2b"

Where NanoID shines:

  • Short URLs and slugs
  • Client-side generated IDs (smaller payload)
  • Session tokens and temporary identifiers
  • When you need a custom alphabet or length

The common mistake with NanoID:

Developers sometimes reduce the length for aesthetics without understanding the collision math:

// DON'T DO THIS for primary keys
const id = nanoid(8); // Only 48 bits of entropy!

An 8-character NanoID has roughly 2^48 possible values. That sounds like a lot until you consider the birthday problem — with just ~17 million IDs, you have a 1% chance of collision. For a primary key, that’s unacceptable.

Rule of thumb: Never go below 21 characters for NanoID if uniqueness matters. If you need shorter IDs, use them as display identifiers, not primary keys.

ULID: The Best of Both Worlds?

ULID (Universally Unique Lexicographically Sortable Identifier) combines a 48-bit timestamp with 80 bits of randomness:

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

This gives you two crucial properties that UUID v4 lacks:

  1. Sortability — ULIDs generated later are always lexicographically greater
  2. Timestamp extraction — you can derive when the ID was created
import { ulid, decodeTime } from 'ulid';

const id = ulid(); // "01ARZ3NDEKTSV4RRFFQ69G5FAV"
const timestamp = decodeTime(id); // 1469918176385

Why Sortability Matters for Databases

When IDs are time-sorted, new inserts always go to the end of the B-tree index. This means:

  • No page splits
  • Hot data stays in cache
  • Sequential disk writes (much faster)
  • ORDER BY id gives you chronological order for free

I now use ULID for any table that will grow beyond a few hundred thousand rows. The performance difference is measurable and consistent.

The ULID Gotcha: Monotonicity

Within the same millisecond, ULID’s randomness component should be monotonically increasing to maintain sort order. Not all implementations handle this correctly.

// Good implementation: monotonic ULID
import { monotonicFactory } from 'ulid';
const ulid = monotonicFactory();

// These will be correctly ordered even within the same ms
const id1 = ulid(); // 01BX5ZZKBKACTAV9WEVGEMMVRY
const id2 = ulid(); // 01BX5ZZKBKACTAV9WEVGEMMVRZ (incremented)
const id3 = ulid(); // 01BX5ZZKBKACTAV9WEVGEMMVS0 (incremented)

If your ULID library doesn’t support monotonic generation, IDs created in the same millisecond will have random ordering — which partially defeats the purpose. Always use the monotonic variant for database keys.

Common Mistakes I’ve Seen (and Made)

Mistake 1: Using UUIDs as URL Slugs

/blog/550e8400-e29b-41d4-a716-446655440000

UUIDs are ugly in URLs, hard to read over the phone, and waste characters. Use NanoID for URL-facing identifiers, or even better, use meaningful slugs with a short random suffix:

/blog/json-vs-yaml-config-mistakes-xK9f2

Mistake 2: Exposing Timestamps in ULIDs to Users

Since anyone can extract the creation timestamp from a ULID, don’t use them as user-facing IDs if the creation time is sensitive information. For example, if a ULID is used as an order ID, competitors could estimate your order volume by comparing timestamps.

Use ULID for internal primary keys and NanoID for external-facing identifiers.

Mistake 3: Storing UUIDs as Strings

If you’re using PostgreSQL, always use the native UUID type, not VARCHAR(36):

-- Wrong: wastes space and is slower to compare
CREATE TABLE users (
    id VARCHAR(36) PRIMARY KEY
);

-- Right: stored as 16 bytes, indexed efficiently
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid()
);

A UUID column takes 16 bytes. A VARCHAR(36) takes 37+ bytes and requires string comparison instead of binary comparison. Over millions of rows, this difference is substantial.

Mistake 4: Not Considering Your Database’s Native Support

  • PostgreSQL has native UUID type and gen_random_uuid() — use them
  • MySQL 8+ has UUID_TO_BIN() and BIN_TO_UUID() for efficient storage
  • MongoDB uses ObjectId by default, which is already time-sortable (similar philosophy to ULID)
  • SQLite has no native UUID type — store as BLOB(16) or TEXT

Don’t fight your database. If it has built-in ID generation that fits your needs, use it.

My Recommendation

After trying all three across different projects, here’s what I reach for:

Use CaseMy ChoiceWhy
Database primary key (high volume)ULIDSortable, great index performance
Database primary key (low volume)UUID v4Built-in, universally understood
URL identifiersNanoID (21 chars)Short, URL-safe, no dependencies
API tokens / session IDsNanoID (32+ chars)Configurable entropy
Distributed systemsUUID v7 (if available)Standardized, time-sorted
Quick prototypingUUID v4Zero setup

One thing worth mentioning: UUID v7 is the new kid on the block. It’s essentially what ULID does (timestamp + random), but as an official RFC standard. If your language/framework supports it, UUID v7 might be the future default that makes this entire comparison obsolete.

Generate and Test

If you want to experiment with UUIDs, check out our UUID Generator — it supports different versions, custom separators, and bulk generation so you can see the format differences firsthand. For generating secure random strings for tokens, the Password Generator lets you control length, character sets, and entropy.


Part of our Developer Generators Guide series. For securing your generated tokens, read Password Security: What Every Developer Should Know.

Necmeddin Cunedioglu
Necmeddin Cunedioglu Author

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.