Auto-increment, UUID, ULID and Snowflake-style ids
coreintermediateFour schemes cover almost every identifier you will generate, and they differ in who has to be involved to produce one. An auto-increment id is assigned by the database from a sequence: it is compact, ordered by creation, and requires the database to have seen the row, which means you cannot know the id before inserting and you cannot generate ids independently in two databases without them colliding. It is also guessable and it leaks volume — `/orders/1041` tells a reader roughly how many orders exist. A UUID is 128 bits generated anywhere with no coordination at all; version 4 is random, which makes it unguessable and unordered, and that lack of order is what makes it awkward as a primary key in a B-tree index. A ULID is 128 bits split into a 48-bit timestamp followed by 80 random bits, so ids sort by creation time while staying independently generatable — the same idea standardised as UUID version 7 in RFC 9562. A Snowflake-style id is a 64-bit integer packing a timestamp, a machine or shard identifier and a per-millisecond sequence counter, giving compact, time-ordered ids that need coordination only once, when a machine is assigned its identifier. The choice is not about taste: it is about whether you need to generate ids before writing, whether they must sort by time, whether they must be unguessable, and how much index locality you are willing to give up.
Think of it as
Think of who signs the ticket. An auto-increment id is signed by a single clerk, so the numbers are neat and everything queues at that desk. A UUIDv4 is a number everyone picks at random from a space so vast that collisions are not worth worrying about — nobody queues, and nothing is in order. A ULID or UUIDv7 is the same self-service idea with the date written first, so the pile sorts itself. A Snowflake id is self-service too, but each desk was given a number in advance, so the desk identifier is baked into every ticket and the ids stay short. The question is never "which is best" — it is "how much coordination can this system afford at the moment an id is needed".
What we're doing: Pick a scheme for four real situations by asking one question each.
- 5
- The most common mistake in this list is not choosing wrongly here but choosing UUIDs by reflex. An internal id nobody outside the database ever sees gains nothing from being 128 bits and unguessable, and pays for it on every index page.
- 13
- Time-ordering is what makes the unguessable option affordable: it keeps inserts landing at the end of the index rather than scattered across it, which is the specific cost the next concept quantifies.
- 26
- Size only becomes the deciding factor at volume. Eight bytes saved per id is invisible at a million rows and is a hundred gigabytes a year at this rate — which is also why the coordination cost of assigning machine ids becomes worth paying.
Why this works: Each situation is decided by one question — is the id needed before the insert, is enumeration a risk, is the volume high enough for size to matter — rather than by a general ranking of schemes. Working the questions in that order gives a defensible answer and avoids the two default failures: sequential ids in public URLs, and random UUIDs as a clustered key on a high-insert table.
Exposing auto-increment ids in public URLs
Wrong
Better
What you see: Competitors quote your monthly order volume accurately, having read it off two order numbers a week apart. If object-level authorization is missing anywhere, the same sequential ids turn a single leaked URL into a complete data export.
Why: A sequential id is a public counter, so it discloses volume and growth rate to anyone who can obtain two of them. It also makes enumeration free, which turns any missing authorization check from a bug affecting one record into a bug affecting every record — an unguessable id does not fix the authorization gap, but it removes the cheap way to exploit it.
- UUIDv4: No coordination, Unordered — generate anywhere, sorts nowhere
- ULID / UUIDv7: No coordination, Time-ordered — generate anywhere, sorts by creation time
- Snowflake: between No coordination and Coordination per id, Time-ordered — one-time machine-id assignment, then independent
- Auto-increment: Coordination per id, Time-ordered — the database assigns every id
The four schemes at a glance
Snowflake-style bit layout (a common 64-bit arrangement)
Remember: Four schemes, distinguished by how much coordination an id costs to make. Auto-increment is smallest and ordered but needs the database and is guessable, so keep it internal. UUIDv4 needs no coordination and sorts nowhere. ULID and UUIDv7 put a millisecond timestamp in front of random bits, so ids are independently generatable, unguessable and still time-ordered — the usual choice for anything public or client-generated. Snowflake-style packs time, machine id and sequence into 64 bits for compact ordered ids at high volume, at the cost of assigning machine ids and depending on a clock that never steps backward.
See also: randomness sortability coordination and index locality · entities and identifiers · clock drift and wall clock time · choosing shard keys · stable identifiers and error contracts

