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

5 April, 2026 Backend

I first reached for NanoID when I needed short, URL-safe identifiers for a link shortener. UUIDs were too long, ULIDs still felt verbose, and auto-increment leaked business data. NanoID solved the problem in 21 characters with zero configuration. But it is not always the right choice - here is how the three formats compare and when each one wins.

What Is NanoID?

NanoID is a tiny string ID generator (130 bytes gzipped in JavaScript) that produces URL-safe random identifiers. The default output is 21 characters using an alphabet of 64 URL-safe characters: A-Za-z0-9_-.

V1StGXR8_Z5jdHi6B-myT

Unlike UUID, NanoID has no internal structure - no version bits, no timestamp, no variant field. Every character is pure randomness, which means the full length contributes to collision resistance. The default 21 characters at 64 possible values per character give 126 bits of entropy - comparable to UUID v4's 122 random bits, but in a much shorter string.

Generate a NanoID directly in your browser.

Custom Alphabets and Lengths

NanoID's most distinctive feature is alphabet customisation. You can generate IDs using any character set:

import { customAlphabet } from 'nanoid';

// Numeric-only IDs (e.g. for verification codes)
const numericId = customAlphabet('0123456789', 12);
numericId(); // '839174652031'

// Lowercase + digits only (case-insensitive systems)
const lowerId = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789', 16);
lowerId(); // 'k7f2m9x4p1b8v3n6'

// Emoji IDs (yes, really)
const emojiId = customAlphabet('🐢🐱🐭🐹🐰🦊🐻🐼', 6);
emojiId(); // '🐼🐢🦊🐱🐻🐹'

This flexibility is useful when IDs must conform to external constraints - DNS labels, case-insensitive file systems, human-readable codes, or systems that reject certain characters.


Side-by-Side Comparison

Feature NanoID UUID v4 ULID UUID v7
Default length 21 chars 36 chars 26 chars 36 chars
Random bits 126 122 80 62
URL-safe Yes No (- only) Yes No
Sortable No No Yes Yes
Timestamp embedded No No Yes (48-bit ms) Yes (48-bit ms)
Custom alphabet Yes No No No
Custom length Yes No (fixed 128-bit) No (fixed 128-bit) No (fixed 128-bit)
Spec Community RFC 4122 Community RFC 9562
Native DB type No Yes (uuid) No Yes (uuid)
Exposes creation time No No Yes Yes

The standout difference: NanoID is the only format where you control both the length and the alphabet. UUID and ULID are fixed at 128 bits with predefined encodings.


Collision Probability

All random ID generators share the same birthday problem math. The question is always: "How many IDs can I generate before the probability of a collision becomes concerning?"

The Math

For n random IDs with k possible values, the collision probability is approximately:

P β‰ˆ nΒ² / (2 * k)

For NanoID (default 21 chars, alphabet of 64):

  • Possible values: 64^21 β‰ˆ 4.7 Γ— 10^37
  • At 1 million IDs per second, you would need ~1,000 years to reach a 1% collision probability

For UUID v4 (122 random bits):

  • Possible values: 2^122 β‰ˆ 5.3 Γ— 10^36
  • Similar order of magnitude to NanoID default

For ULID (80 random bits per millisecond):

  • Possible values per millisecond: 2^80 β‰ˆ 1.2 Γ— 10^24
  • Much smaller random space, but scoped to a single millisecond - collisions only happen between IDs generated in the same millisecond

In practice, none of these formats have a collision problem at any realistic scale. The database unique constraint is your safety net regardless.

Adjusting NanoID Length

If you shorten NanoID, collision resistance drops fast:

Length Entropy (bits) IDs before 1% collision risk
8 48 ~19 million
12 72 ~73 billion
16 96 ~280 trillion
21 (default) 126 ~4.6 Γ— 10^18

For a link shortener with millions of URLs, 12 characters is sufficient. For a distributed system generating billions of events, stick with 21 or longer.


URL Safety and Compactness

NanoID was designed for URLs from the start. Its default alphabet (A-Za-z0-9_-) requires no percent-encoding in any part of a URL - path segments, query parameters, or fragments.

Compare the same ID in a URL:

# NanoID (21 chars, no encoding needed)
/api/orders/V1StGXR8_Z5jdHi6B-myT

# UUID v4 (36 chars, dashes are safe but verbose)
/api/orders/f47ac10b-58cc-4372-a567-0e02b2c3d479

# ULID (26 chars, uppercase but URL-safe)
/api/orders/01ARZ3NDEKTSV4RRFFQ69G5FAV

NanoID is 41% shorter than UUID and 19% shorter than ULID. In contexts where ID length matters - short URLs, QR codes, SMS messages, log lines - this adds up.


Database Considerations

No Native Type

Unlike UUID, which has a native column type in PostgreSQL and MySQL 8, NanoID is stored as VARCHAR. This has implications:

  • Storage: A 21-character NanoID in VARCHAR(21) takes 22-23 bytes (depending on the database's length prefix). A UUID in the native uuid type takes exactly 16 bytes. For tables with hundreds of millions of rows, the difference in index size matters.
  • Index performance: String comparisons are slower than binary comparisons. The difference is negligible for small tables but becomes measurable at scale.
  • No random ordering problem: Like UUID v4, NanoID is fully random and causes B-tree index fragmentation on write-heavy tables. Unlike ULID or UUID v7, there is no timestamp prefix to maintain insert order. If you use MongoDB, ObjectID already embeds a timestamp β€” NanoID would lose that benefit.

When NanoID Works Well in a Database

  • Tables under ~10 million rows where index fragmentation is not a bottleneck
  • Systems where the ID is primarily used in application code and APIs, not for range scans
  • Cases where a short, URL-safe ID is the primary requirement and you accept the storage trade-off

When to Prefer UUID or ULID

  • Write-heavy tables above 10 million rows - use UUID v7 or ULID for time-ordered inserts
  • PostgreSQL-centric stacks where the native uuid type gives you 16-byte storage and built-in operators
  • Systems that need to extract creation timestamps from IDs without a database query

Code Examples

JavaScript / TypeScript

// npm install nanoid
import { nanoid, customAlphabet } from 'nanoid';

// Default: 21 chars, URL-safe
const id = nanoid();       // 'V1StGXR8_Z5jdHi6B-myT'

// Custom length
const short = nanoid(12);  // 'k7f2m9x4p1b8'

// Custom alphabet + length
const hexId = customAlphabet('0123456789abcdef', 16);
hexId();                   // 'a3f8c2e1b9d04f67'

// Async version (uses crypto.getRandomValues internally)
import { nanoid } from 'nanoid';
const asyncId = nanoid();  // same API, async under the hood in some environments

PHP

// composer require hidehalo/nanoid-php
use Hidehalo\Nanoid\Client;

$client = new Client();

// Default 21 chars
$id = $client->generateId();
// 'V1StGXR8_Z5jdHi6B-myT'

// Custom length
$shortId = $client->generateId(size: 12);
// 'k7f2m9x4p1b8'

// Custom alphabet
$client = new Client();
$numericId = $client->formattedId(alphabet: '0123456789', size: 10);
// '8391746520'

Python

# pip install nanoid
from nanoid import generate

# Default 21 chars, URL-safe
id = generate()          # 'V1StGXR8_Z5jdHi6B-myT'

# Custom length
short = generate(size=12)  # 'k7f2m9x4p1b8'

# Custom alphabet + length
numeric = generate('0123456789', 10)  # '8391746520'

NanoID vs UUID vs ULID: When to Use Each

Choose NanoID when:

  • You need short, URL-safe IDs and control over length and alphabet
  • The ID appears in user-facing URLs, QR codes, or space-constrained contexts
  • You are building a JavaScript/TypeScript application where the tiny bundle size matters
  • Sortability and embedded timestamps are not required

Choose UUID v4 when:

  • Maximum ecosystem compatibility is the priority - every database, ORM, and API understands UUIDs
  • You need opaque IDs that reveal nothing about creation time
  • You are working with legacy systems that expect RFC 4122 format

Choose ULID or UUID v7 when:

  • You need time-ordered IDs for database write performance
  • Extracting creation timestamps from IDs is valuable for your use case
  • You want the B-tree-friendly insert pattern without sacrificing uniqueness

If you want to understand the differences between all UUID versions β€” including v1, v3, v5, and the newer v7 β€” or see a deeper comparison of UUID and ULID with database benchmarks and security trade-offs, those articles cover the internals in detail.


Which One to Pick

NanoID fills a gap that UUID and ULID leave open: short, customisable, URL-safe random identifiers. Its trade-off is clear β€” no timestamp, no native database type, no RFC standard. For public-facing IDs where brevity matters and you do not need time ordering, it is the most practical choice. For everything else, UUID v7 is the safer default.

More Articles

CSV vs JSON for Data Exchange: When Each Format Wins

A practical comparison of CSV and JSON for APIs, data pipelines, and file exports. Covers structure, parsing, streaming, schema enforcement, size, tooling, and clear guidelines for choosing the right format.

15 April, 2026

SEO for AI Search: How to Optimise for ChatGPT, Perplexity, and Google AI Overviews

How AI-powered search engines discover, evaluate, and cite web content. Practical strategies for optimising your pages for ChatGPT Browse, Perplexity, Google AI Overviews, and other AI answer engines.

14 April, 2026

Image to Base64 Data URIs: When to Inline and When Not To

A practical guide to embedding images as Base64 data URIs. Covers the data URI format, size overhead, performance trade-offs, browser caching, Content Security Policy, and clear rules for when inlining helps vs hurts.

10 April, 2026