Picking an ID scheme: UUID v4 / v7, ULID, NanoID, and Snowflake by use case

6 min read

“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

SchemeLengthEncodingTime embeddedSortableRandomnessPrimary use case
UUID v436 (hex)hex + hyphensnoneno122 bitsanonymous IDs, general-purpose
UUID v736 (hex)hex + hyphens48-bit msyes74 bitsDB keys, log IDs
ULID26 (Crockford Base32)Base3248-bit msyes80 bitsDB keys, IDs that go in URLs
NanoID21 (configurable)URL-safe Base64noneno~126 bitsshort URLs, public-facing tokens
Snowflake19-digit number64-bit integer41-bit msyes22 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 UUID columns 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 UUID type, 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 BIGINT column.
  • 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:

  1. Must the ID fit in a numeric column (legacy DB, JOIN performance)?

    • Yes → Snowflake.
    • No → continue.
  2. Do you want time-sortability (DB keys, log streams)?

    • Yes → UUID v7 (interop) / ULID (length, readability).
    • No → continue.
  3. Must the generation source stay hidden (no leaked timestamp)?

    • Yes → UUID v4 or NanoID.
    • No → reconsider 2.
  4. 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.