Why you need a plan for unique IDs, and when to use each scheme
Every record needs an ID: an order, a message, a user, a short link. On one database the answer is easy: an auto-increment column that counts 1, 2, 3. But once many servers create records at the same time, or the data is split across several databases, that one counter becomes a problem. Two servers can pick the same number, or every write has to wait for the one machine that owns the counter.
A good ID scheme gives you two things interviewers always ask about:
- Uniqueness at scale: no two records ever get the same ID, even when many machines create IDs at once.
- Availability: creating an ID keeps working when one machine, one datacentre or one clock misbehaves.
Many designs also want a third property: IDs that sort by creation time, which is kind to database indexes and makes "newest first" cheap.
Use a dedicated scheme when:
- Several servers or services create records at once and you cannot ask one database for every number.
- The data is sharded (split across databases), so each shard cannot count on its own.
- You need IDs that are short (for a link), sortable by time, or hard to guess.
- Clients, such as mobile apps working offline, must create IDs themselves.
Skip it, or use something simpler, when:
- One database holds all the data and one writer is enough: a plain auto-increment or identity column is the simplest choice.
- The IDs never leave one table and nobody cares about their order or size.
- You are early on: start simple, and move the day the counter becomes the bottleneck.
Try it
Three machines, A, B and C, ask for IDs every millisecond. Choose how the IDs are made, then inject a failure at tick 6 and watch what it does to uniqueness. Rows that would share an ID are marked as duplicates.
Generating unique IDs on three machines
Step 1 of 15 · Press Space to play, arrow keys to stepNo IDs yet. Press Play.
Each machine builds IDs itself from three parts: the time in milliseconds, its own machine number, and a sequence number for IDs made in the same millisecond. No machine needs to talk to another. One tick is one millisecond. Press Play to see three machines (A, B and C) ask for IDs.
- IDs made:
- 0
- duplicates:
- 0
- requests delayed:
- 0
- in time order:
- -
1 tick = 1 millisecond. A duplicate means two records would share an ID. In time order counts how often an ID is bigger than the one made just before it.
Things to try:
- Snowflake, no failure: every ID is unique and each one is bigger than the one before. Open the details of a row: it is built from the time, the machine number and a sequence.
- Random UUID: no duplicates and no coordination, but the "in time order" counter shows the IDs have no order at all.
- Central counter, then "Counter goes down": IDs are perfectly ordered until tick 6, then every request is refused. One counter is a single point of failure.
- Snowflake, "A clock jumps back 3 ms", then "Carry on regardless": machine B reuses times it has already used, and duplicate IDs appear. Switch to "Wait until it catches up": no duplicates, at the cost of some delayed requests.
- Snowflake, "Two machines share an ID": a mistake in configuration breaks uniqueness, with no clock involved.
ID generation strategies
| Strategy | How it makes an ID | Good when | Weakness |
|---|---|---|---|
| Database auto-increment | The database counts 1, 2, 3 | One database, one writer | Bottleneck and single point of failure. Collides across shards. |
| Ticket servers | A few counters step by the number of servers (odd and even) | You need small, ordered-ish numbers and can run two or three counters | An extra service to run. Adding a server changes the step. |
| Random UUID (version 4) | 122 random bits in a 128-bit value | Any machine, even a client, must create IDs with no coordination | 128 bits. No order, so inserts land at random places in an index. |
| Time-ordered UUID (version 7) or ULID | A timestamp first, then random bits | You want no coordination and time order | 128 bits. Exact order within one millisecond is not guaranteed. |
| Snowflake-style | Timestamp, machine number and sequence in 64 bits | High volume, time order, small IDs | Needs a unique machine number per generator and trustworthy clocks. |
| Block allocation | A central counter hands each server a block, such as 1,000 numbers | The counter must be called rarely | Unused numbers in a block are lost if a server crashes. |
Coordinated vs uncoordinated generation
This is the main fork in the road.
- Coordinated schemes (a counter, ticket servers, block allocation) get numbers from a shared service. IDs are small and ordered, but the service is a bottleneck and a single point of failure that you must keep available.
- Uncoordinated schemes (random UUIDs, Snowflake) let every machine make IDs on its own. Nothing blocks, and nothing is shared at request time. The price is larger IDs (UUIDs) or a setup step and a reliance on clocks (Snowflake).
Snowflake sits in the middle: the only coordination is once, when each machine is given its machine number.
How Snowflake-style IDs work
A Snowflake-style ID is one 64-bit number with three parts, in this order:
- Timestamp (41 bits): milliseconds since a chosen start date. 41 bits last about 69 years.
- Machine number (10 bits): which generator made it. 10 bits allow 1,024 generators.
- Sequence (12 bits): a counter for IDs made in the same millisecond by the same machine. 12 bits allow 4,096 IDs per millisecond. It resets to 0 when the millisecond changes.
(The layout also keeps one unused sign bit at the front, which makes 1 + 41 + 10 + 12 = 64.)
Why it is unique: two IDs from different machines differ in the machine bits. Two IDs from the same machine in different milliseconds differ in the time bits. Two in the same millisecond differ in the sequence. Because the time comes first, IDs are roughly sorted by creation time.
Two things can break it:
- A clock that goes backwards. If the clock is corrected to an earlier time, the generator can reuse a timestamp and a sequence number it has already used. A safe generator remembers the last timestamp it used and waits until its clock passes it, or refuses to make IDs and raises an alert.
- Two machines with the same machine number. Nothing in the ID itself can stop this, so machine numbers must be handed out by something reliable, such as a configuration service, or leased from a coordination service.
If more than 4,096 IDs are wanted in one millisecond, the generator waits for the next millisecond.
Unique IDs vs the alternatives
| Concern | Snowflake-style | Central counter or ticket server | Random UUID | Time-ordered UUID |
|---|---|---|---|---|
| Needs coordination while running | No | Yes | No | No |
| Survives a machine or datacentre failure | Yes | Only with several counters | Yes | Yes |
| Sorts by creation time | Yes | Yes | No | Yes |
| Size | 64 bits | Small number | 128 bits | 128 bits |
| Depends on clocks | Yes | No | No | Yes |
| Hard to guess | Somewhat | No | Yes | Somewhat |
| Extra moving part | Machine numbers | A counter service | None | None |
| Best for | High-volume services | Small systems, short links | Client-made IDs, public IDs | Databases that like ordered keys |
Snowflake-style IDs win for high-volume services that want small, ordered IDs. A random UUID wins when simplicity matters more than size. A plain counter is enough until it is not.
Avoiding a new single point of failure
A counter is the classic new single point of failure: if it is down, nobody can create a record. Real setups protect it:
- Several ticket servers that step by the number of servers: with two, one hands out odd numbers and the other even numbers. If one is down, the other still works, and the IDs stay unique. The trade-off is that adding a third server later means changing the step, so plan the step with room to grow.
- Block allocation: each application server asks for a block of, say, 1,000 numbers and uses them locally, so the counter is called once per block, not once per ID. If the counter is down, servers keep going until their block is empty. The trade-off is that a crashed server loses its unused numbers, which is fine because IDs do not need to be gapless.
- No central service at all: use a scheme where each machine makes its own IDs, and give each machine a number once.
The same worry applies to machine numbers: if they come from a service, that service must not be needed on every request. Fetch the number once at start-up and cache it.
Common mistakes
- Using an auto-increment for IDs that appear in URLs. Sequential IDs reveal how many records exist and are easy to guess. Use a random or scrambled ID for public values.
- Trusting the clock. NTP (the protocol that corrects clocks) can step a clock backwards. Remember the last timestamp and refuse to go back.
- Giving two machines the same machine number, for example by copying a configuration file. Assign numbers from one source and check for clashes at start-up.
- Random UUIDs as the primary key of a big table. Inserts land at random places in the index. Use a time-ordered ID for the primary key, and keep random ones for public values.
- Using 32-bit integers. A 32-bit counter runs out at about 4 billion. Use 64 bits.
- Expecting gapless IDs. Block allocation, crashes and rolled-back transactions all leave gaps. Never build logic that needs numbers to be consecutive.
How to talk about unique IDs in an interview
- Say the requirements first: IDs per second, whether they must sort by time, how long they can be, and whether they may be guessed.
- Start simple: "one database auto-increment works until it becomes the bottleneck", then explain why it breaks with many writers or shards.
- Pick one option and say why: for a high-volume service, "I would use Snowflake-style IDs: 64 bits, roughly time-ordered, no coordination at request time".
- Mention failure unprompted: the clock going backwards and duplicate machine numbers, and how you guard against both.
- Name the trade-off: coordination and a single point of failure against larger IDs and a reliance on clocks.