Picking an identifier scheme has more options than people realize: UUID v4, UUID v7, ULID, Snowflake, nanoid — each trades off on sortability, database friendliness, URL safety, and predictability. This guide compares the five you'll actually consider and gives a recipe for which one to pick when.
Why the choice matters
Identifiers are expensive to migrate. Once an ID format is baked into URLs, logs, analytics, and backups, swapping it out is a multi-quarter project. And the shape of an identifier — random vs sortable, short vs long, client- vs server-generated — affects:
- Index efficiency. B-tree indexes love sorted keys. Random UUID v4 as a primary key scatters insertions across the index and tanks write throughput and cache locality.
- URL safety. Auto-increment integers leak record counts and invite enumeration. Random-enough IDs avoid both.
- Debuggability. Time-sortable IDs let you eyeball recency in logs without a separate timestamp column.
The five contenders
1. UUID v4 (random)
f47ac10b-58cc-4372-a567-0e02b2c3d479- Layout: 122 bits of randomness + 6 bits version/ variant. 128 bits total.
- Format: 32 hex chars + 4 dashes = 36 chars.
- Sortable: No.
- Collisions: Effectively zero. Generate a trillion and the expected collisions sit around 0.0000003.
- Pros: Generate anywhere — client, server, DB — with no coordination. Universal library support.
- Cons: Fragments DB indexes. Long.
2. UUID v7 (timestamp + random)
018f5b86-7c4d-7abc-9def-0123456789ab- Layout: 48-bit Unix millisecond + 12-bit random + 62-bit random. Standardized in RFC 9562 (2024).
- Format: Same 36 chars as v4.
- Sortable: Yes, by millisecond. Random within the same ms.
- Collisions: 74 random bits within a ms — effectively zero.
- Pros: Drop-in UUID compatibility plus sortability. Friendly to DB indexes. The recommended default for new systems.
- Cons: Leaks creation time — if hiding "when was this made" matters, use v4 or store the time separately.
3. ULID
01HQXM5K3D7Z9YW0123456789AB- Layout: 48-bit ms + 80-bit random = 128 bits.
- Format: Crockford Base32, 26 chars, no dashes.
- Sortable: Lexicographically. A plain string sort gives time order.
- Pros: URL-friendly (alphanumeric only). 28% shorter than UUID. Convertible 1:1 to UUID (both are 128 bits).
- Cons: Not a native DB type — usually VARCHAR(26) or BINARY(16). UUID v7 standardization eroded some of ULID's edge.
4. Snowflake (Twitter)
1768956123987456000- Layout: 41-bit timestamp + 10-bit machine id + 12-bit sequence = 63 bits (signed 64-bit).
- Format: Integer, ~19 chars when stringified.
- Sortable: Yes.
- Pros: Fastest indexes (integer). Stores in 8 bytes as BIGINT. Short URLs.
- Cons: Needs machine-id assignment infrastructure. Must handle clock rollbacks. Cannot generate on clients without machine-id collisions.
5. nanoid (short random)
V1StGXR8_Z5jdHi6B-myT- Layout: 21 chars default, 126 bits of randomness, URL-safe alphabet.
- Sortable: No.
- Pros: Shorter than UUID v4 with equivalent security.
- Cons: No native DB type. Smaller tooling ecosystem.
At a glance
| UUID v4 | UUID v7 | ULID | Snowflake | |
|---|---|---|---|---|
| Length | 36 | 36 | 26 | ~19 |
| Random bits | 122 | ~74 + ms | 80 + ms | ~12 + ms |
| Sortable | No | Yes (ms) | Yes (ms) | Yes |
| Leaks time | No | Yes | Yes | Yes |
| Distributed | Yes | Yes | Yes | Needs infra |
| DB type | UUID | UUID | BINARY/VARCHAR | BIGINT |
Picking one
New primary keys
Default to UUID v7. You keep the standard UUID type and get time-sortable indexes for free. Libraries: Node uuid@9+, Python uuid_utils, Go google/uuid@1.6+. yutils' UUID / ULID Generator also generates v7.
Public URLs
If leaking creation time is a concern (signup time, e.g.), use UUID v4 or nanoid. Otherwise UUID v7 or ULID is fine. Never expose auto-increment integers in public URLs — attackers can guess the next id and estimate your record count.
Log / event ids
ULID is the sweet spot — short, sortable, URL-friendly. You can eyeball log order from the id alone, no separate timestamp column needed.
High-throughput PKs
Snowflake or BIGINT auto-increment. UUID v7 is fast enough for most workloads, but the BIGINT (8 bytes) vs UUID (16 bytes) difference matters at the largest scales. Only viable if all IDs are server-generated.
Offline-first / client-generated
UUID v4 or v7. No machine-id coordination, zero collisions. Standard for mobile/PWA apps that create records offline and sync later.
Pitfalls
1. UUID v1 (MAC + timestamp)
Common in old systems but discouraged: leaks the MAC address (privacy) and can collide when generated rapidly on the same machine. UUID v7 is the modern replacement.
2. Storing UUIDs as strings
PostgreSQL and MySQL UUID types are 16-byte binary. Storing the 36-char string form bloats the index 2.25× and slows queries. Always use the native UUID type.
3. ULID monotonicity
ULID is monotonic within a millisecond only if the library's monotonic mode is explicitly enabled. Otherwise it's random within the ms — sort order is guaranteed only at millisecond granularity.
4. Overestimating collision risk
With UUID v4's 122 random bits, generating a million IDs per second for a century still gives you well under a 50% collision chance. Adding UNIQUE constraints and retry loops "just in case" is usually overkill for v4 — but deterministic UUID v5 (seed-based) is a different story.
Try it
- UUID / ULID Generator — generate UUID v4, v7, and ULID in batches.
- Unix Timestamp Converter — verify the timestamp extracted from a v7 or ULID.
Recap
- Default new PKs to UUID v7 — UUID compatibility + sortable indexes + distributed generation.
- For public URLs, pick v7/ULID when leaking creation time is fine, v4/nanoid when it isn't.
- ULID is the sweet spot for log / event ids.
- Store UUIDs as native binary, not strings.
- Don't expose auto-increment integers in public URLs — enumeration risk.