Skip to main content

Reference: Unique ID Generation

A reference on generating unique IDs without coordination — auto-increment, hashing, UUIDs, Snowflake, and range allocation — and which to pick for each job.

What it is

Almost every system needs an identifier it can mint without asking a single global database: a short URL code, a message id, an order id. The design question is how to get uniqueness without coordination, and which of three further properties you need: whether ids are sortable by time, whether they are small (for example a 64-bit integer), and whether they are guessable (a security question).

Important

Key takeaway: Any scheme that says “generate, check for a collision, then write” has a check-then-write race: two servers can both find no collision and both write. Eliminate collisions by construction (a counter, or time plus machine plus sequence), not by hope.

When you care

It appears inside the URL shortener, chat, orders, and feeds. The signal is dismissing the weak options with specific failure modes before choosing.

Note

Interview signal: Walking through hashing and random ids, dismissing each with a concrete concurrency failure, then landing on range-allocated counters or Snowflake-style ids is the move. Jumping straight to “I’d use a counter” without saying why the others fail misses the point of the question.

The options

SchemeHowSortable by timeSizeCollisionsNeeds coordination
Auto-increment (database sequence)One counter in one databaseYes64-bitNoneYes: a single bottleneck and failure point
Hash of the contentMD5 or SHA, truncated and encodedNoChosenYes, and a check-then-write raceNo
Random (UUID v4)122 random bitsNo128-bitNegligibleNo
Time-ordered random (ULID, UUID v7)A timestamp prefix plus randomnessYes128-bitNegligibleNo
Snowflake-styleTimestamp, machine id, sequence packed in 64 bitsYes64-bitNone, by constructionA one-time machine-id assignment
Range allocationA central allocator hands each server a block of counter valuesRoughly64-bitNoneRarely: once per block

Hash and random short codes fail the same way. A 7-character base62 code has 62⁷ ≈ 3.5 trillion values, so a random code at 1 billion stored has about a 0.03% chance of collision per attempt. That sounds negligible, but the check for it (“is this code taken?”) then the write are two steps, and two servers doing it at once both pass the check. Hashing a long URL has the same race and, worse, a collision on content, so you must probe and retry. Both turn a uniqueness problem into a concurrency problem.

Range allocation (counter blocks)

Each application server asks a central allocator (a database row with SELECT ... FOR UPDATE, ZooKeeper, or a small service) for a block of, say, 1,000 values, then hands them out locally with no coordination. When the block runs out it asks for another. Encode the integer in base62 for a short code. The allocator is touched once per 1,000 ids, so one allocator supports huge write rates, and ids are unique by construction. If a server dies, the unused remainder of its block is simply skipped. Out-of-order ids are fine when the id only needs to be unique.

Snowflake-style ids

A 64-bit id built from three fields:

| 41 bits: milliseconds since a custom epoch | 10 bits: machine id | 12 bits: sequence |

Worked example. 41 bits of milliseconds is about 69 years from the custom epoch; 10 bits allows 1,024 machines; 12 bits allows 4,096 ids per millisecond per machine, about 4 million per second per machine. Because the timestamp is the high bits, ids sort by time, which gives chat and feeds a free ordering key and keeps database inserts mostly appending to the right edge of an index (good for B-trees).

Failure modes to name: clock moving backwards (an NTP step) can mint a duplicate or an out-of-order id, so the generator refuses to issue ids until the clock catches up or uses the last timestamp; machine-id collisions (two machines configured with the same id) cause duplicates, so machine ids come from a registry; and the sequence resets each millisecond.

Time-ordered random ids (ULID, UUID v7) put a timestamp in the high bits and randomness after it, giving sortability and no machine-id registry, at the cost of 128 bits.

Warning

Production reality: Sortable ids leak information. A timestamp prefix reveals when something was created, and sequential ids reveal volume (“order 100,482” tells a competitor how many orders you took). If ids are exposed externally, add a random component or map internal ids to opaque public ones.

When to pick what

SituationChooseWhy
Short URL codeRange-allocated counter, base62Unique by construction, tiny allocator load
Message or event ids that must order within a conversationSnowflake-style or ULIDTime-sortable and unique
A single database, modest scaleAuto-incrementSimplest
Ids from many independent writers, no registryUUID v4, or v7 if sortability mattersNo coordination; negligible collisions
Public identifiers that must not be guessableRandom, or opaque mappingNo enumeration
Idempotency keysClient-generated UUIDUnique per logical operation

Caution

Common mistake: Claiming a hash or a random id is “collision-free”. It is collision-unlikely, and the check-then-write step reintroduces the race. If uniqueness is a correctness property (a short code, a primary key), enforce it with a unique constraint or build it in by construction.