Dev Hub Solutions

Product studio

Get in touch
7 min readdatabases / uuid / fundamentals

Database primary keys in 2026: int, UUID v4, v7, ULID, NanoID, KSUID

Six common primary key types, six tradeoffs. Here's when each one wins, with the specific failure modes that bite at scale.

"Use a UUID" used to be the right answer. In 2026 it's "which UUID — and have you considered ULID, NanoID, or KSUID instead?" Each option has a specific scaling failure mode you should know before you commit a 50-million-row table to it.

The candidates

Type Width Sortable URL-safe shape Library universal
Auto-increment int 4-8 bytes Yes Bad (/users/12847) Yes
UUID v4 16 bytes No OK (/users/9e0c2a4b-...) Yes
UUID v7 16 bytes Yes (by time) OK Yes
ULID 16 bytes Yes (by time) Better (01HK3X...) Mostly
NanoID Variable No Best (/users/abc123XYZ_) Yes
KSUID 20 bytes Yes (by time) Good Yes

Auto-increment int

The classic. Compact, fast, sequential — every database is optimised for this case. B-tree inserts cluster at the right edge, cache stays hot, throughput is maximal.

The problem is everything outside the database. URLs that expose /users/1, /users/2, /users/3 invite enumeration attacks. Anyone can probe /users/N for the next ID. Worse, they reveal business metrics — competitors can sign up at 9am and 5pm, subtract user IDs, and get your daily growth rate.

Use when: internal-only tables, audit logs, references that never appear in URLs or external APIs.

Avoid when: anything user-facing, anything with security implications around enumeration.

UUID v4

122 bits of randomness. Solves the enumeration problem completely — no two adjacent v4s tell you anything about the system. The downside is insert performance at scale: random IDs scatter writes across every page of the index, killing cache locality and halving (or worse) write throughput on large tables.

For tables under a few million rows, v4 is fine. Past that, the B-tree pathology becomes measurable. Our UUID v7 vs v4 post goes deep on the math.

Use when: tables that stay small, anything that must be unpredictable (tokens, session IDs, password reset codes).

Avoid when: high-insert-rate primary keys past low millions of rows.

UUID v7

The fix for v4's insert problem. First 48 bits encode a Unix millisecond timestamp; remaining 74 bits are random. Result: still globally unique, still unpredictable enough for most uses, but sorts chronologically — so inserts cluster like auto-increment.

Standardised in RFC 9562 in May 2024. Library support is now near-universal (Postgres 18+ native, Python 3.14+ standard library, Node uuid v9+, Go google/uuid v1.6+, Rust uuid 1.10+). Our UUID generator does v7 in bulk; the per-language pages have code examples.

Use when: high-insert primary keys in modern databases. This is the right default for new schemas in 2026.

Avoid when: identifiers that need to be timing-side-channel-free (security tokens — the first 48 bits leak issue time).

ULID

Same idea as UUID v7 but predates it (2016). 128 bits — 48 of timestamp, 80 of randomness — encoded in Crockford Base32 (26 characters, no I, L, O, U to avoid confusion). The encoding is the win: ULIDs look like 01HK3XQYV8DJWPZ40RFQM9X2T7 — shorter than UUID's hyphenated hex, lexicographically sortable as strings (so they work in any store that sorts by string, including key-value databases and NoSQL).

The catch: smaller adoption than UUID. Some database drivers handle ULID natively; many don't (you store as text or as bytes). The benefit only pays off if your stack treats them as first-class.

Use when: NoSQL key stores (DynamoDB sort keys, Redis sorted sets), event streams, anything that sorts as strings.

Avoid when: Postgres/MySQL with strong UUID support — UUID v7 gives you the same benefits with better native handling.

NanoID

A small, customisable alphabet-encoded random ID. Defaults to 21 characters, ~126 bits of entropy. Configurable: pick your alphabet, pick your length. URL-shape output is the cleanest of any of these (/users/V1StGXR8_Z5jdHi6B-myT).

No timestamp, no sortability. Pure randomness in a compact alphabet.

Use when: short URLs, public-facing slugs, anywhere the visual shape matters (/share/abc123XYZ reads better than /share/9e0c2a4b-8d3f-4b6e-9c1a-1f2b3c4d5e6f).

Avoid when: primary keys at scale (same B-tree problem as v4), anything that benefits from sortability.

KSUID

Segment's contribution (2017). 160 bits — 32 of timestamp, 128 of randomness. Sortable like v7/ULID, with more randomness per ID.

The 32-bit timestamp uses second resolution (vs ULID/v7's millisecond), which means you can't reliably sort IDs minted in the same second by creation order. Higher randomness compensates for collisions but not for ordering.

Use when: you want extra collision-resistance and don't mind 4 extra bytes per ID; legacy Go services where KSUID is already in use.

Avoid when: you need millisecond-precise creation order (use v7); you care about minimising bytes per ID (use v7).

Quick decision

  • High-insert primary keys, modern database, greenfield: UUID v7. Standardised, well-supported, sortable, secure-enough.
  • Need unguessable tokens (session, password reset, magic link): UUID v4 or NanoID. Don't leak the timestamp.
  • NoSQL / key-value, sortable strings matter: ULID.
  • Public-facing short URLs: NanoID.
  • Internal-only, append-only tables: Auto-increment int.
  • Audit-log style with collision paranoia: KSUID.

For mixed workloads, you'll often use two. Internal tables on auto-increment int; user-facing entities on UUID v7; share URLs on NanoID; auth tokens on v4 or NanoID. Don't try to make one ID type carry every use case.

Try the generator

Our UUID generator produces v4 and v7 in bulk with one-click copy and CSV export. Per-language code is at /uuid-generator/[python|javascript|go|rust|...] for v4 and v7 generation.