
UUID, ULID, NanoID and CUID2: which ID should you use in 2026?
A complete guide with JavaScript and TypeScript examples on UUID v4/v7, ULID, NanoID and CUID2.
The ID problem in modern systems
Every time you design a system, sooner or later you run into the same question: how do I uniquely identify my entities? An auto-increment database ID is fine for a small site, but the moment you have multiple instances, replicas, or need to generate IDs client-side, things get complicated.
There are three concrete problems with sequential IDs. First: they leak information — if your order is ORDER-00042, a malicious user knows how many orders you have. Second: you can’t generate them without hitting the database, which creates a bottleneck in distributed systems. Third: in multi-tenant or sharded environments, ID conflicts are a real risk.
That’s where UUID, ULID and friends come in.
Context
Every format covered here produces globally unique IDs with no central coordination. That means you can generate them in any process, at any time, with no risk of collisions — or nearly none.
UUID — the timeless classic
UUID stands for Universally Unique Identifier. It’s been around since the ’80s and is standardized as RFC 4122. It’s a 128-bit number represented as a hex string with dashes:
550e8400-e29b-41d4-a716-446655440000
Several versions exist. The three you’ll actually run into are v1, v4, and the more recent v7.
UUID v4 — the most common
UUID v4 is purely random. 122 bits of randomness (the remaining 6 bits are reserved for the format spec). The collision probability is astronomically low: you’d need to generate about a billion UUIDs per second for 85 years before hitting a 50% chance of collision. In practice, you don’t think about it.
In Node.js, from version 14.17 on, you don’t need external libraries:
// Native Node.js — no dependency
import { randomUUID } from "node:crypto";
const id = randomUUID();
// "f47ac10b-58cc-4372-a567-0e02b2c3d479"
// Or with the uuid library, if you want more flexibility
import { v4 as uuidv4, v1 as uuidv1, v7 as uuidv7 } from "uuid";
const idV4 = uuidv4(); // random
const idV1 = uuidv1(); // timestamp-based (de facto deprecated)
const idV7 = uuidv7(); // timestamp + randomness (the future)
UUID v7 — the sortable future (already here)
UUID v7 was standardized in RFC 9562 in 2024. It solves v4’s biggest problem: ordering. The first 48 bits hold a Unix timestamp in milliseconds, which means two UUID v7s generated in sequence are chronologically sortable. This makes a huge difference for write performance on databases with B-Tree indexes (like PostgreSQL or MySQL), because inserts don’t cause index fragmentation.
import { v7 as uuidv7 } from "uuid";
// Generate two IDs in quick succession
const id1 = uuidv7();
const id2 = uuidv7();
console.log(id1); // "018e6a3f-5c00-7a1b-b2c3-d479f47ac10b"
console.log(id2); // "018e6a3f-5c01-7d2e-a3b4-e589f58bd21c"
// Lexicographic ordering reflects creation order
console.log(id1 < id2); // true ✓
// You can extract the timestamp from the ID
function extractTimestampFromUUIDv7(uuid: string): Date {
const hex = uuid.replace(/-/g, "").slice(0, 12);
const ms = parseInt(hex, 16);
return new Date(ms);
}
My take
If you’re starting from scratch today and choosing to use UUID, go straight to v7. Library support is already there, databases handle it like any other UUID, and you save yourself the headaches of v4’s non-deterministic ordering in queries with ORDER BY created_at.
ULID — when order matters
ULID stands for Universally Unique Lexicographically Sortable Identifier. It was designed specifically to solve the problem UUID v4 ignores: ordering.
01ARZ3NDEKTSV4RRFFQ69G5FAV
A ULID is 128 bits total: the first 48 bits are a millisecond timestamp, the last 80 bits are random. The result is a 26-character string in Crockford Base32 encoding (an alphabet without the letters I, L, O, U, which are easily confused).
It has two advantages over UUID. First: it’s lexicographically sortable — you can sort ULIDs as strings and get chronological order. Second: it’s more compact — 26 characters versus UUID’s 36.
import { ulid, decodeTime } from "ulid";
// npm install ulid
// Basic generation
const id = ulid();
// "01ARZ3NDEKTSV4RRFFQ69G5FAV"
// You can also pass a specific timestamp
const idWithTimestamp = ulid(Date.now());
// Extracting the timestamp is straightforward
const timestamp = decodeTime(id);
console.log(new Date(timestamp));
// 2025-01-15T10:30:00.000Z
// Lexicographic sort = chronological sort
const ids = [ulid(), ulid(), ulid()];
const sorted = [...ids].sort();
// sorted is already in the correct creation order ✓
ULID and databases: watch how you store it
Here’s a detail that’s often overlooked. If you store a ULID as a string in PostgreSQL or MySQL, you lose the ordering advantage at the B-Tree index level (which operates efficiently on native types). The better approach is to store ULID as bytea in PostgreSQL or as binary(16) in MySQL. Some ORMs like Prisma or TypeORM have native support or plugins for this.
import { ulid, decodeTime } from "ulid";
// ULID ↔ Buffer conversion for efficient storage
function ulidToBuffer(id: string): Buffer {
const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
let value = 0n;
for (const char of id.toUpperCase()) {
value = value * 32n + BigInt(ENCODING.indexOf(char));
}
const buf = Buffer.alloc(16);
for (let i = 15; i >= 0; i--) {
buf[i] = Number(value & 0xffn);
value >>= 8n;
}
return buf;
}
// In Prisma, with an @db.Binary(16) field
// await prisma.user.create({ data: { id: ulidToBuffer(ulid()) } })
Heads up
ULID generates at most 1000 IDs per millisecond with guaranteed randomness. If you need to generate more IDs in the same millisecond, the random part gets incremented — but if you exceed 2^80 in a single millisecond (impossible in practice), it wraps back to zero. Not a real-world problem, but worth knowing.
NanoID — compact and fast
NanoID isn’t a “standard format” like UUID or ULID. It’s a JavaScript/TypeScript library that generates random IDs with a configurable alphabet and length. The default produces 21-character strings using a URL-safe alphabet:
V1StGXR8_Z5jdHi6B-myT
The library is tiny (under 130 bytes minified + gzipped), uses crypto.getRandomValues for cryptographic randomness, and works in both Node.js and the browser with no changes needed.
import { nanoid, customAlphabet } from "nanoid";
// npm install nanoid
// Basic use — 21 URL-safe characters
const id = nanoid();
// "V1StGXR8_Z5jdHi6B-myT"
// Custom length
const shortId = nanoid(10);
// "IRFa-VaY2b"
// Custom alphabet — handy for user-readable codes
const numericOnly = customAlphabet("0123456789", 8);
const orderCode = numericOnly();
// "47291083"
// Confirmation code with no ambiguous characters (0/O, 1/l/I)
const safeCode = customAlphabet("23456789ABCDEFGHJKLMNPQRSTUVWXYZ", 8);
console.log(safeCode()); // "A3KM9P7R"
// Collision probability calculation with the official library:
// https://zelark.github.io/nano-id-cc/
// With 21 chars and a 64-char alphabet: ~149 years at 1000 IDs/sec for a 1% collision chance
When NanoID makes sense
NanoID shines when you need short, readable IDs — promo codes, session tokens, shortened links, IDs exposed in public URLs. It’s not meant to be a database primary key (it lacks sortability and a standardized structure), but for these specific use cases it’s unbeatable.
My take
I personally find NanoID great when I need to expose an ID in a URL or show it to the user. Being able to define the alphabet is the killer feature: I can strip out ambiguous characters (0, O, 1, I, l) and get codes the user can type by hand without cursing.
CUID2 — built for databases
CUID (Collision-resistant Unique ID) arrived with a specific promise: to be optimized for databases and horizontally scalable systems. Version 2 — CUID2 — is a complete 2022 rewrite that dropped the fixed prefix and improved randomness.
clh3eke620000356ok53fgx3e
A CUID2 is always lowercase, 24 characters long by default (configurable), always starts with a letter (handy for using it as a variable name or CSS class), and includes a machine fingerprint to further reduce collisions in distributed environments.
import { createId, init, isCuid } from "@paralleldrive/cuid2";
// npm install @paralleldrive/cuid2
// Basic use
const id = createId();
// "clh3eke620000356ok53fgx3e"
// Custom configuration
const createShortId = init({
length: 16,
// custom fingerprint per service instance
fingerprint: process.env.SERVICE_NAME ?? "default",
});
const shortId = createShortId();
// "c8k2nm4pq9rst6uv"
// Validation — useful for sanitizing API input
function validateUserId(id: unknown): string {
if (typeof id !== "string" || !isCuid(id)) {
throw new Error("Invalid user ID");
}
return id;
}
Tip
CUID2 includes a “fingerprint” based on the current process by default, which helps reduce collisions on multi-process systems with no coordination. You can override it with a service identifier to make the system even more predictable.
CUID2 with Prisma
Prisma supports CUID2 natively as an ID generator in schema.prisma:
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
}
// For CUID2 specifically with Prisma 5+:
model Post {
id String @id @default(cuid(2))
}
Direct comparison
Putting it all in one table, because at some point you just want to see the numbers:
| Format | Length | Sortable | Timestamp | Standard | Browser | Main use case |
|---|---|---|---|---|---|---|
| UUID v4 | 36 chars | ❌ | ❌ | RFC 4122 | ✅ | Compatibility, interoperability |
| UUID v7 | 36 chars | ✅ | ✅ (ms) | RFC 9562 | ✅ | Modern primary key |
| ULID | 26 chars | ✅ | ✅ (ms) | Open spec | ✅ | Event sourcing, logs, timeseries |
| NanoID | 21 chars* | ❌ | ❌ | ❌ | ✅ | URLs, tokens, user codes |
| CUID2 | 24 chars* | ❌ | ❌ | ❌ | ✅ | Databases, APIs, ORMs |
* configurable length
A generation benchmark
The numbers vary machine to machine, but the relative proportions are fairly stable. I measured on Node.js 22, generating 1,000,000 IDs per type:
randomUUID() → ~9.8M op/s
uuid v4 → ~7.2M
uuid v7 → ~6.8M
ulid → ~4.1M
nanoid → ~3.9M
cuid2 → ~1.2M
CUID2 is slower because it uses SHA-3 internally to strengthen randomness. But consider that in an HTTP endpoint the bottleneck is the database query, not ID generation.
Pros and cons at a glance
UUID v4
Pros
- Native support in all modern databases
- RFC standard — maximum interoperability
- No dependency needed in Node.js 14.17+
- Mature, ubiquitous tooling
- Simple regex validation
Cons
- Not sortable — fragments B-Tree indexes
- 36 characters — verbose in URLs and logs
- No embedded time information
- Limited human readability
UUID v7
Pros
- Chronologically sortable
- RFC 9562 standard — already supported
- Compatible with existing UUID columns
- Timestamp extractable with no extra query
- Better B-Tree index performance
Cons
- Still 36 characters with dashes
- Native DB support still not uniform
- (Partially) reveals creation time
ULID
Pros
- Lexicographically sortable
- Compact — 26 chars vs UUID’s 36
- Crockford alphabet with no ambiguous characters
- Embedded timestamp with ms precision
- Great for event sourcing and logs
Cons
- Not a formal standard (RFC)
- Native DB support almost nonexistent
- Needs care for efficient storage
- Reveals creation timestamp
NanoID
Pros
- Compact and customizable
- Works in the browser with no changes
- Configurable alphabet — perfect for readable codes
- Tiny bundle size (<130 bytes gz)
- Cryptographically secure randomness
Cons
- No temporal ordering
- No standard — custom validation
- Not suited as a DB primary key
- No embedded timestamp
CUID2
Pros
- Optimized for databases and sharding
- Process fingerprint reduces collisions
- Always starts with a letter — HTML/CSS safe
- Configurable length
- Native Prisma integration
Cons
- Slowest to generate
- No formal standard
- No temporal ordering
- Requires an external dependency
What I usually use, and why
-
For database primary keys I use UUID v7. The reason is simple: it’s an open standard, databases handle it natively as a UUID type, and the ordering advantage on indexes is concrete — noticeable on tables above a million rows. I consider UUID v4 essentially outdated for new projects.
-
For IDs exposed in public URLs or tokens I use NanoID, typically with a custom alphabet that excludes ambiguous characters. 21 characters are enough randomness, and the compactness matters when the ID ends up in a log or a shared link.
-
For event-sourcing systems, or where I need ordering and a timestamp without hitting the database, I use ULID. It’s the most elegant format of the group for this category of problem.
-
I use CUID2 mainly when working with Prisma on projects where the native integration is worth the simplicity. It’s not my first choice outside that context.
A quick decision tree:
*Do you need a primary key in a relational database?*
→ UUID v7 (or CUID2 if you use Prisma)
*Do you need an ID in a URL or visible to the user?*
→ NanoID with a custom alphabet
*Are you building event sourcing, or do you need temporal ordering without a DB?*
→ ULID
*Do you need to integrate with third-party systems that speak UUID?*
→ UUID v4 (for compatibility) or v7 (if they support it)
There’s no “universally correct” choice. There’s the right choice for your context. If you’re starting a new project in 2026, UUID v7 covers 90% of use cases with no wasted time. Start there and only change when you have a concrete reason.
Reference libraries
# UUID — all versions including v7
npm install uuid
npm install --save-dev @types/uuid
# ULID
npm install ulid
# NanoID (native ESM from v4 on)
npm install nanoid
# CUID2
npm install @paralleldrive/cuid2
# Native UUID v4 (no dependencies, Node.js 14.17+)
import { randomUUID } from 'node:crypto';
If you’re working with Node.js, also keep an eye on the official security releases: generating solid IDs doesn’t help much if you’re then running vulnerable versions.
Final tip
Start with UUID v7 and only change if you need to.