Diego Betto's Blog
Photo by Ricardo Gomez Angel on Unsplash

April 14, 2026 · 12 min di lettura

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.

Condividi:XLinkedInFacebookWhatsApp

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.

Condividi:XLinkedInFacebookWhatsApp