Should your primary key be an integer that counts up, a 128-bit UUID, or a human-readable slug? The uuid vs auto-increment debate usually gets framed as a database-only question, but the answer changes once you have public URLs, API resources, and more than one server writing rows. These three strategies solve overlapping but distinct problems. Most production systems end up using all of them at different layers.

Auto-increment — simple, sequential, and leaky

An auto-increment column is a 64-bit integer the database hands out in order. Every insert takes the next number. Joins on integers are fast, the index stays compact, and the value is readable at a glance in a debug log.

The tradeoffs are well known but worth naming. /users/1042 tells anyone how many users signed up before this one; competitors have scraped growth curves from exactly that. If your IDs are exposed in URLs or APIs, an attacker can walk the sequence: /orders/1001, /orders/1002, /orders/1003 until something interesting appears. Auto-increment also assumes a single writer. The moment you shard across two databases or accept writes in two regions, you need a coordinator or a range allocator, and you’ve reintroduced the central bottleneck the sharding was supposed to remove.

UUID — globally unique, no coordinator

A UUID is a 128-bit identifier generated independently by any node, with collision probability low enough to ignore. Specified in RFC 9562 (which replaces the older RFC 4122), UUIDs come in versions tuned for different needs.

Two versions matter in practice. UUIDv4 is 122 bits of cryptographic randomness: unguessable, no ordering, the workhorse for decades. UUIDv7 is the modern default for new systems: it packs a millisecond timestamp into the high bits and randomness into the low bits, so IDs are still globally unique but also time-sortable. Time ordering matters because a UUIDv4 primary key indexes badly. Random inserts fragment the B-tree, and you lose the “recent rows cluster together” property that auto-increment gives you for free. UUIDv7 gives you both global uniqueness and monotonic insert order. The deeper v4-vs-v7 comparison, with index-size measurements, is in UUID v4 vs v7.

The cost is size. A UUID is 16 bytes, double a 64-bit integer, and it shows up in every foreign key and every index. In a 50-million-row graph of join tables, that adds up. UUIDs are also ugly in a URL. /posts/017b4d6e-1f2a-7c01-9a44-d1a2b3c4d5e6 tells a reader nothing and a search engine even less.

Slug — readable, indexable, and mutable

A slug is a short, human-readable string derived from content: /blog/uuid-vs-autoincrement-vs-slug. It carries meaning an integer or UUID cannot. Search engines weight URL words as a mild ranking signal, users can remember or guess the address, and a shared link previews its topic before you click.

The tradeoff is that slugs are mutable in a way an integer is not. Titles get edited, content gets repurposed, and now your slug needs either a redirect story or a permanent guarantee of immutability. You also need a uniqueness rule: two posts titled “Hello World” cannot both live at /hello-world. Common patterns are appending a discriminator (/hello-world-2) or storing the slug alongside a stable integer or UUID primary key, so the slug is a lookup field rather than the identifier of record.

Slugs are also expensive to index relative to integers, and they are terrible as foreign keys. Use them where humans and crawlers look (the URL bar) and nowhere else.

A decision table

Pick the identifier that matches the layer. Primary keys care about insert performance and join cost; public URLs care about readability and ranking; API resource ids care about unguessability and federation; sharded systems care about avoiding a central coordinator.

Use caseRecommended identifierWhy
Database primary keyUUIDv7 (or auto-increment on a single-node system)Sortable, unique without a coordinator, decent insert locality
Public-facing URLSlug backed by a UUIDv7 primary keyReadable for users and search engines; stable id underneath
API resource idUUIDv7Unguessable, no enumeration, safe to expose across services
Sharded / multi-region writesUUIDv7 or UUIDv4Each node generates independently, no central counter
Secret token (session, reset link)Not an id — use a random secretUUIDs are identifiers, not credentials

The last row is the one that bites teams. A UUID identifies a row; it does not authorize access to it. Session tokens, password-reset links, and API keys need cryptographic randomness sized for the threat, 128 bits minimum, generated for that purpose. If you need one, a password generator gives you a strong random string and is a better mental model than reaching for a UUID and hoping.

Generate UUIDs

For most new code, UUIDv7 as the primary key and a slug layered on top for any URL humans will see is the combination that ages well. You get global uniqueness, time-sortable indexes, no central coordinator, and a URL that reads as English. Paste a UUID Generator if you want to inspect the format or grab one quickly. It produces v4 and v7 so you can see the timestamp-prefixed difference in the first segment.

FAQ

Which UUID version should I use for a new primary key?

UUIDv7 in almost all cases. It is globally unique like v4, but the embedded timestamp means inserts land in sorted order, which keeps B-tree indexes healthy. Reach for v4 only when you specifically want IDs that carry no timing information.

Can I use a slug as my primary key?

You can, but you usually shouldn’t. Slugs are mutable and longer than an integer or UUID, which bloats every foreign key and index that references them. The common pattern is a UUID or integer primary key plus a separate indexed slug column used only for URL lookups, with redirects in place if a slug ever changes.

Are UUIDs safe to use as session tokens or passwords?

No. A UUID is an identifier, not a secret. RFC 9562 explicitly warns that UUIDs are not security primitives and must not be used as authentication credentials. Generate session tokens, reset links, and API keys with a dedicated random source — at least 128 bits of entropy from a password generator or equivalent.

Summary

The rule is simple. Auto-increment when you have one writer and want fast, compact integers, and you accept that row counts leak. UUIDv7 when you need global uniqueness without a coordinator, sortable inserts, and safe exposure in APIs. A slug when the identifier will appear in a URL that humans and search engines read. Most systems want all three at once — a UUIDv7 primary key, a slug column for public paths, and an auto-increment only where a single-node legacy schema demands it. Never use any of them as a password; that is a different job entirely.