Why Your Choice of ID Generator Has Architectural Consequences
Every persistent entity in a software system needs a unique identifier. The simplest approach — an auto-incrementing integer — works well for single-database, single-server applications. But the moment your system distributes across multiple services, allows offline creation of records, exposes IDs in URLs, or needs to merge data from disparate sources, sequential integers become a liability. They leak information about record counts, they require a centralized sequence generator (a bottleneck and a single point of failure), and they create merge conflicts when multiple sources generate IDs independently.
The response to these problems is **distributed unique ID generation** — producing identifiers that are globally unique without coordination between generators. Three formats dominate this space in modern software development: **UUID v4**, **ULID**, and **NanoID**. Each solves the core problem differently, with distinct tradeoffs around sortability, length, collision probability, readability, and database performance.
The stakes of this choice are higher than they appear. If you are using UUIDs as **database primary keys**, the random distribution of UUID v4 values causes severe **index fragmentation** in B-tree indexes (used by PostgreSQL, MySQL InnoDB, and SQL Server). Every new row inserts into a random location in the index rather than appending to the end, causing frequent page splits and degraded write performance at scale. This is not theoretical — benchmarks on tables exceeding 10 million rows show UUID v4 primary keys performing **50-70% slower on writes** compared to sequential alternatives.
Conversely, if your IDs appear in **URLs or user-facing interfaces**, UUID's canonical hyphenated format (`550e8400-e29b-41d4-a716-446655440000`) is verbose and carries no semantic meaning. NanoID's compact alphanumeric format (`V1StGXR8_Z5jdHi6B-myT`) is 36 characters compressed to 21 while maintaining similar entropy, making it dramatically more practical for URLs, QR codes, and short-link systems.
Understanding the architectural implications before committing to an ID format is essential because migrating primary key formats in an existing production database is one of the most painful operations in software engineering.
UUID v4: The Universal Standard and Its Hidden Costs
UUID v4 (**Universally Unique Identifier, version 4**) is defined by RFC 4122 and generates 128 bits of data, with 122 bits random and 6 bits used for version and variant flags. Its canonical string representation uses 32 hexadecimal characters and 4 hyphens: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` where the `4` indicates version 4 and `y` is one of `8`, `9`, `a`, or `b`.
**Collision probability** is the headline advantage. With 122 bits of randomness, the number of UUIDs needed before you have a 50% chance of even a single collision is approximately **2.71 × 10^18** — roughly 2.71 quintillion. At a generation rate of 1 billion UUIDs per second, you would need to run for **86 years** before collision becomes likely. For virtually all practical applications, UUID v4 collision risk is zero.
However, the **database performance problem** is real and measurable. PostgreSQL's `uuid` type stores UUIDs efficiently as 16-byte binary values, but the random distribution across the key space means every INSERT lands at a random position in the primary key B-tree index. This shatters **locality of reference** — the property that recently inserted rows should be physically close in the index — and causes constant cache misses and disk seeks. The practical impact:
``` Benchmark: 50M rows, PostgreSQL 16, NVMe SSD Format | Write throughput | Index size | Seq scan time ---------------|-------------------|------------|--------------- SERIAL INT | 95,000 rows/sec | 1.1 GB | 42s UUID v4 | 38,000 rows/sec | 2.8 GB | 71s UUID v7 | 88,000 rows/sec | 1.3 GB | 45s ULID | 86,000 rows/sec | 1.3 GB | 44s ```
UUID v4 also **leaks nothing** about creation time or ordering, which is both a privacy feature and a debugging limitation. When troubleshooting production issues, being able to sort records by ID to see creation order is surprisingly useful — UUID v4 makes that impossible without a separate `created_at` column and associated query overhead.
**UUID v7**, finalized in RFC 9562 (2024), addresses the sortability problem while maintaining the 128-bit UUID format. It uses a **48-bit millisecond Unix timestamp** prefix followed by 80 bits of random data, making UUIDs lexicographically sortable by creation time. UUID v7 support is landing in major databases: PostgreSQL 17 includes a native `gen_random_uuid()` equivalent for v7, and most UUID libraries have added v7 support through 2024-2025. For new projects that need UUID compatibility (existing tooling, RFC compliance, 128-bit identifiers), **UUID v7 is now the recommended choice** over v4.
ULID: Sortable IDs for Database-Friendly Distributed Systems
**ULID (Universally Unique Lexicographically Sortable Identifier)** was designed explicitly to solve the database performance problem of random UUIDs. A ULID consists of 128 bits encoded as a 26-character base32 string (Crockford's base32 alphabet, which is URL-safe and case-insensitive): `01ARZ3NDEKTSV4RRFFQ69G5FAV`.
The structure is: ``` 01ARZ3NDEK TSV4RRFFQ69G5FAV |---------| |----------------| 48 bits 80 bits Timestamp Randomness (millisecond) ```
The **48-bit millisecond timestamp prefix** means ULIDs generated in the same millisecond sort lexicographically close together, and ULIDs generated in later milliseconds sort after earlier ones. This near-monotonic property is the key to eliminating B-tree index fragmentation — new records consistently append near the end of the index rather than scattering randomly.
The 80 random bits give a collision probability similar to UUID v4 **within a single millisecond** across multiple generators. If two generators create a ULID at the exact same millisecond (which is rare but possible), there is still a 1-in-2^80 chance of collision — approximately 1.2 × 10^24 combinations, more than sufficient for any production system.
One nuance: the ULID specification includes an **optional monotonic sort order** mode. When generating multiple ULIDs within the same millisecond, standard ULID would regenerate the random bits each time, which means ULIDs from the same millisecond could sort in arbitrary order. The monotonic mode increments the random portion by 1 for each successive ULID within the same millisecond, ensuring strict ordering even at high generation rates. Most mature ULID libraries expose a `monotonicFactory()` or equivalent.
**When to choose ULID:** - You need sortable IDs and database write performance is critical - You want timestamp extractability from IDs (useful for time-range queries using only the ID column) - Your stack does not yet support UUID v7 natively - You want a URL-safe format without hyphens
**When ULID is suboptimal:** - You need RFC 4122 UUID format compatibility (ULID is not a UUID) - Your system generates IDs at rates exceeding a few million per second (monotonic mode may run out of increment space within a millisecond) - Team members are unfamiliar with the format, causing maintenance friction
NanoID: Compact, URL-Safe IDs for Modern Web Applications
**NanoID** takes a different approach to the ID problem. Rather than maximizing bit-width or timestamp sortability, it optimizes for **compactness and readability** while maintaining configurable security. The default NanoID generates a 21-character string using a 64-character alphabet (`A-Za-z0-9_-`), yielding approximately **126 bits of entropy** — comparable to UUID v4's 122 bits of effective randomness.
The 21-character default ID (`V1StGXR8_Z5jdHi6B-myT`) achieves the same collision resistance as UUID v4 in roughly **half the characters** (21 vs 36 without hyphens, or 32 in hex). This compactness matters for several use cases:
- **URL slugs**: `https://example.com/files/V1StGXR8_Z5jdHi6B-myT` is more readable and shareable than a full UUID - **QR codes**: Shorter URLs produce simpler QR codes with lower error-correction requirements - **Short links and share tokens**: Users see and sometimes type these values; shorter is dramatically better UX - **Log correlation IDs**: Request trace IDs and session IDs appear constantly in logs; compact format reduces noise
NanoID is also **highly configurable**. You can adjust both the alphabet and the length to tune entropy and format for your use case:
```js import { customAlphabet } from 'nanoid';
// Numbers-only, 10 digits — for user-visible order numbers const orderIdGen = customAlphabet('0123456789', 10); console.log(orderIdGen()); // '4815162342'
// URL-safe, 32 chars — high-entropy for security tokens const tokenGen = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 32);
// Readable, no ambiguous chars (0/O/l/I removed) — for invite codes const inviteGen = customAlphabet('23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghjkmnpqrstwxyz', 8); ```
The **collision probability formula** for NanoID (or any custom configuration): `P ≈ t^2 / (2 × A^N)` where `t` is the number of IDs generated, `A` is the alphabet size, and `N` is the ID length. For the default 21-character NanoID at a rate of 1,000 IDs per second, you would need **~41 million years** before collision probability reaches 1%.
NanoID uses `crypto.getRandomValues()` in browsers and `crypto.randomFillSync()` in Node.js, making it cryptographically secure by default. It has official implementations in over 20 languages, making it suitable for polyglot systems where ID generation must be consistent across services.
Collision Probability, Entropy, and the Math Behind Safe ID Generation
Making an informed choice between ID formats requires understanding the underlying mathematics, not just trusting marketing claims about "virtually zero collision risk." The **birthday problem** — also called the birthday paradox — is the governing formula. It states that in a set of N randomly chosen values from a space of size M, the probability of at least one collision follows:
``` P(collision) ≈ 1 - e^(-N^2 / 2M)
Or rearranged: N ≈ sqrt(2M × ln(1/(1-P))) ```
For P = 0.5 (50% chance of collision), N ≈ 1.18 × sqrt(M). This means collisions become likely when the number of generated IDs approaches the square root of the total ID space.
Applying this to our three formats:
| Format | Effective bits | Space (M) | IDs for 1% collision | |--------|----------------|-----------|----------------------| | NanoID 21 | 126 | 8.5 × 10^37 | 1.3 × 10^17 | | UUID v4 | 122 | 5.3 × 10^36 | 3.3 × 10^16 | | ULID (random part only) | 80 | 1.2 × 10^24 | 4.9 × 10^10 | | NanoID 10 (digits only) | 33 | 10^10 | 446 |
The ULID row deserves careful reading: if you are comparing ULIDs generated **within the same millisecond**, only the 80 random bits distinguish them. At 49 billion ULIDs generated in a single millisecond, there is a 1% collision risk. This is an astronomical rate no real system approaches, but it illustrates why high-rate systems should use monotonic mode.
The 10-digit NanoID row is a warning about **misconfigured custom alphabets**. A 10-digit numeric ID (like an order number) has only 10 billion possible values. At just 446 generated IDs, collision risk reaches 1%. If you expose such IDs publicly, even a modest-traffic system will experience collisions within months. The safe minimum for a randomly generated, database-unique ID is roughly **60 bits of entropy** — anything less requires uniqueness enforcement at the database level (unique constraints plus retry logic).
For developers building ID generation logic, the practical rule is: **if in doubt, add length**. The storage and bandwidth cost of a few extra characters is negligible. The debugging cost of a production collision is not.
Database Integration Patterns: Indexing, Storage, and Query Performance
Choosing an ID format is only half the battle — integrating it correctly with your database layer determines whether you capture the performance benefits in practice. Several common mistakes can negate the advantages of a sortable ID format.
**PostgreSQL considerations:**
- UUID v4 should use the native `uuid` type (16 bytes), not `varchar(36)` (36 bytes + overhead). The difference compounds: a 50M-row table with `varchar(36)` primary keys consumes roughly **1.5GB more index space** than the native type. - For UUID v7 and ULID in PostgreSQL, the `uuid` type still works if you can convert the format. Alternatively, use a `text` or `char(26)` column for ULID — it stores and sorts correctly because base32 encoding is lexicographically consistent. - Enable the `pg_uuidv7` extension (available in PostgreSQL 17+) for native v7 generation: `SELECT gen_random_uuid_v7()`
**MySQL/MariaDB considerations:**
- InnoDB's clustered primary key makes ID choice especially critical. The entire table is physically ordered by primary key — a randomly distributed UUID v4 primary key causes **worst-case index fragmentation** in InnoDB more severely than in PostgreSQL's heap-based storage. - Use `BINARY(16)` for UUID storage, not `CHAR(36)`, and apply the transformation `UUID_TO_BIN(uuid, 1)` (the `1` flag enables bit rearrangement for better sorting) and `BIN_TO_UUID(bin, 1)` for retrieval. This converts UUID v4 to a more sortable byte order. - MySQL 8.4+ includes `UUID_TO_BIN()` and the `IS_UUID()` function natively.
**Indexing strategy for high-write tables:**
```sql -- PostgreSQL: partial index on recent data (frequently queried window) CREATE INDEX idx_events_recent ON events (id) WHERE created_at > NOW() - INTERVAL '30 days';
-- Better: use ULID/UUID v7, rely on natural ordering CREATE TABLE events ( id TEXT PRIMARY KEY, -- ULID, naturally sorted payload JSONB, created_at TIMESTAMPTZ GENERATED ALWAYS AS ( -- Extract timestamp from ULID for indexed time queries to_timestamp(('x' || lpad(encode(decode(substring(id, 1, 10), 'base32'), 'hex'), 16, '0'))::bit(64)::bigint / 1000.0) ) STORED ); ```
**Application layer patterns:** Use a per-service ULID or NanoID factory instance rather than calling the library function directly each time. This allows you to inject a mock factory in tests (returning predictable IDs), configure entropy sources centrally, and add application-level logging of ID generation for debugging collision edge cases.
Practical Decision Guide: Which ID Format to Use When
After covering the technical depth of each format, a practical decision framework helps apply this knowledge to real projects. The right choice depends on four factors: **interoperability requirements**, **database write patterns**, **ID visibility**, and **generation rate**.
**Choose UUID v7 when:** - You need RFC 9562 UUID format compatibility (existing tooling, third-party APIs, regulatory requirements) - Your database layer natively supports UUID type (PostgreSQL, MySQL, SQL Server) - You need sortability and your stack already supports v7 in libraries - You are migrating from UUID v4 and want minimal schema changes
**Choose ULID when:** - Sortable, time-ordered IDs are critical for database performance - You need timestamp extractability from the ID itself (enables range queries on the ID column) - Your system operates in environments where UUID v7 library support is limited - You want a compact, URL-safe format without hyphens that is still human-readable
**Choose NanoID when:** - IDs appear in URLs, QR codes, share links, or any user-visible surface - You need configurable length/alphabet for different use cases (order numbers, invite codes, tokens) - Compactness is a priority (21 chars vs 36 for UUID) - You are building a polyglot system and need consistent generation across many languages
**Keep UUID v4 when:** - You have an existing system with UUID v4 primary keys and migration cost outweighs performance gains - Anonymity of ID generation timing is a privacy requirement (UUID v4 reveals nothing about creation time) - Third-party integrations specifically validate UUID v4 format - Write volume is low enough that index fragmentation is not measurable
**Avoid all three and use sequential integers when:** - Single-database application with no distributed ID generation - Human operators frequently reference IDs directly (integer IDs are dramatically easier to communicate verbally) - Join-heavy query patterns where compact, integer-comparable keys dramatically outperform string keys
The online UUID generator on this site produces RFC-compliant UUID v4 values using `crypto.getRandomValues()` entirely in your browser. For production systems, pair it with the framework above to select the right format before generating your first ID — retrofitting an ID scheme into a production database is far more expensive than designing correctly from the start.
More in generator tools
View all generator tools guides →