Why UUIDs beat auto-increment IDs (and when they don't)
May 13, 2026 · 14 min read
Teams still argue about integer IDs versus UUIDs in 2026 because both sides are right under different constraints. This article is a decision guide, not a slogan. You will leave with criteria you can defend in a design review.
What auto-increment still does better
- Smaller keys - 4 or 8 bytes vs 16.
- Friendly joins - narrow foreign keys, dense indexes.
- Sequential inserts - excellent B-tree locality by default.
- Human support - "ticket #48291" is easier than reading hex.
Where UUIDs clearly win
1. Public, guessable URLs
Sequential IDs leak business metrics (/users/10482 invites scraping). UUIDs remove trivial enumeration.
Pair opaque IDs with authorization, not instead of it.
2. Distributed creation
Mobile apps, edge workers, and microservices can mint IDs without contacting a central sequence. That removes a single-writer bottleneck and failure domain.
3. Data merges and replication
Importing two databases both starting at id=1 is painful. UUID primary keys collide only in theory.
Costs people underestimate
- Index bloat - random v4 hurts write amplification; mitigate with v7/ULID.
- Debugging friction - paste-friendly tools matter (validators, converters).
- Storage overhead - URLs, logs, and JSON payloads grow.
-- PostgreSQL: UUID primary key (consider v7 generators in app layer)
CREATE TABLE orders (
id UUID PRIMARY KEY,
customer_id UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
Hybrid patterns that work
- Internal bigint + public UUID - joins stay fast, APIs stay opaque.
- UUID v7 as PK - keeps sortability without a second column.
- Integer IDs in analytics only - ETL maps UUID to surrogate keys in the warehouse.
Decision checklist
Choose UUIDs when two or more are true:
- IDs appear in client-visible URLs or mobile offline storage.
- Multiple writers generate rows without coordination.
- You merge datasets from independent environments.
- Enumeration risk has a real abuse story in your threat model.
Stick with integers when the database is single-region, internal-only, and join performance dominates.
Worked example: public API resource IDs
Imagine GET /api/invoices/10482. An attacker can scrape 10480-10490 in seconds. Switching to
GET /api/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7 removes trivial enumeration. You still need
authz checks - UUIDs are not magic - but you raise the cost of bulk harvesting.
Internally you might keep invoice_seq BIGSERIAL for reporting joins and expose only the UUID in JSON.
ORMs make this easy with separate id and public_id columns. The extra column is cheaper
than debugging a leaked integer sequence in a compliance review.
interface InvoiceDto {
id: string; // UUID exposed to clients
// internalSeq?: never - not exposed
}
Sharding note: auto-increment per shard still collides when you merge shards unless you allocate non-overlapping ranges. UUIDs (especially v7) simplify merges at the cost of wider keys. Pick based on whether you expect shard merges in the product lifetime.
FAQ
- Are UUIDs slower than integers?
- Joins and index size can be slower with random v4. v7 and proper BINARY(16) storage narrow the gap.
- Should API expose integer or UUID?
- Expose opaque UUID (or ULID) if clients see IDs. Keep integers internal if not.
- Do UUIDs break database replication?
- No - they are values like any other PK. Random v4 can increase write amplification on replicas.
- What about Snowflake IDs?
- Snowflake-style IDs are 64-bit sortable integers - great for high-throughput logs. Use our Snowflake parser when debugging them.
Related: What is a UUID? · Bulk UUID generator