Picking an ID scheme: UUID v4 / v7, ULID, NanoID, and Snowflake by use case
“Auto-incrementing primary key or ULID?” “How short should our short-URL ID be?” “We need IDs that don’t collide across servers.” All of these are ID-scheme decisions. This article lines up the five schemes that are realistic in 2026 — UUID v4, UUID v7, ULID, NanoID, Snowflake — with a comparison table and a decision flow.
Comparison table
| Scheme | Length | Encoding | Time embedded | Sortable | Randomness | Primary use case |
|---|---|---|---|---|---|---|
| UUID v4 | 36 (hex) | hex + hyphens | none | no | 122 bits | anonymous IDs, general-purpose |
| UUID v7 | 36 (hex) | hex + hyphens | 48-bit ms | yes | 74 bits | DB keys, log IDs |
| ULID | 26 (Crockford Base32) | Base32 | 48-bit ms | yes | 80 bits | DB keys, IDs that go in URLs |
| NanoID | 21 (configurable) | URL-safe Base64 | none | no | ~126 bits | short URLs, public-facing tokens |
| Snowflake | 19-digit number | 64-bit integer | 41-bit ms | yes | 22 bits (worker + seq) | numeric primary keys in distributed systems |
The main axes are: do you need time-sortability, do you have a length budget, and do you need a numeric column?
UUID v4: anonymity and pure randomness
Generated by crypto.randomUUID(), 122 bits of randomness.
- Leaks no information about generation order.
- Native support across every language and database.
- B-tree indexes hate it: random insertion order causes page splits, hurting write throughput.
A v4 UUID as a primary key in a high-write table is the canonical “Postgres UUID PK is slow” complaint. Every “UUID is slow” thread is really a v4 thread.
UUID v7: time-ordered, standard-compliant
Formalized in RFC 9562 (2024).
- Top 48 bits = millisecond timestamp.
- Lower 74 bits are random.
- Sortable as a string and as bytes.
- Fits the same 36-char canonical form as v4, so existing
UUIDcolumns Just Work.
The DB performance issue (random inserts splitting pages) is solved. For new projects in 2026, v7 is the default primary-key candidate. Even client-generated v7s sort approximately by time as long as client clocks are roughly correct.
Migration gotcha: v4 → v7
If a UUID column is already populated with v4 values, mixing in v7s does not retroactively make the table sortable. The pragmatic approach: write new rows as v7, leave existing rows as v4, and rely on a separate created_at column for time-ordered queries.
ULID: shorter than UUID, human-friendlier
26 characters in Crockford Base32, e.g. 01ARZ3NDEKTSV4RRFFQ69G5FAV.
- Top 48 bits ms timestamp + 80 bits random — same idea as UUID v7, different spelling.
- Alphabet deliberately omits I, L, O, U to reduce typos.
- No special characters, so it goes directly into URLs.
ULID and UUID v7 are conceptually close. Length: 36 vs 26. UUID v7 wins on binary interop (the uuid column type is everywhere); ULID wins on visible-to-humans contexts.
ULID vs UUID v7: which one?
- You already use the DB native
UUIDtype, or expect to → UUID v7. - IDs are seen by humans (URLs, CLI args, copy-paste) → ULID.
- Storage size is a constraint and you want a fixed 16-byte form → UUID v7.
NanoID: short, URL-safe, customizable
21 chars by default from a 64-character URL-safe alphabet (A-Z a-z 0-9 _ -), pure random.
- Matches UUID-level collision odds in 21 chars.
- Length and alphabet are configurable (12 chars for internal use, drop lowercase for human-readable, etc.).
- No time information.
The right pick for public-facing tokens: short URLs, share links, opaque API IDs.
How short is short enough?
Birthday-bound math: 21 chars (126 bits of entropy) gives billions of years before a collision even at thousands of generations per second. For internal IDs you can shrink it: ~12 chars hits a 0.0001% collision probability after 10 million IDs, which is fine for a URL shortener with manual collision retry.
Snowflake: when you need a 64-bit number
Twitter’s distributed-ID scheme.
- 41-bit ms timestamp + 10-bit worker ID + 12-bit sequence = 64 bits total.
- Single
BIGINTcolumn. - Sortable by time.
When Snowflake fits
- The schema demands a numeric ID (legacy interop, faster JOINs).
- You want decentralized generation — workers mint IDs autonomously without a central numbering service.
- Leaking the timestamp is acceptable.
Snowflake gotchas
- Worker-ID collisions are catastrophic — two workers with the same ID will start producing duplicates.
- The 12-bit sequence caps generation at 4096 IDs per ms per worker. The standard implementation busy-waits 1 ms once the sequence is exhausted.
- Client-side generation with random worker IDs has probabilistic collisions. Snowflake assumes coordinated worker-ID assignment.
Decision flow
Run through these in order for a new system:
Must the ID fit in a numeric column (legacy DB, JOIN performance)?
- Yes → Snowflake.
- No → continue.
Do you want time-sortability (DB keys, log streams)?
- Yes → UUID v7 (interop) / ULID (length, readability).
- No → continue.
Must the generation source stay hidden (no leaked timestamp)?
- Yes → UUID v4 or NanoID.
- No → reconsider 2.
Hard length cap (URL shortener, QR, SMS)?
- Yes → NanoID (≤ 21) or ULID (26).
- No → UUID family is fine.
Recommendations by use case
- New DB primary key → UUID v7.
- DB primary key, numeric required → Snowflake.
- Public-facing short URL → NanoID (12-16 chars).
- Anonymous share link → UUID v4 or NanoID.
- ID that humans copy-paste → ULID (no confusable characters).
- Log / trace ID → UUID v7 or ULID.
Summary
The “always v4” era is ending. Write-heavy primary keys want v7 or ULID, public short tokens want NanoID, numeric-column requirements want Snowflake — that is the standard division of labor in 2026.
To experiment with ULID, NanoID, and Snowflake side by side, the ULID / NanoID / Snowflake generator covers all three. UUID v4 and v7 live in the UUID generator.