Library
28 min read

Design a unique ID generator

Every row in a database needs a name nobody else has: order 1029, user 88, message 7731. That name is its id, and the column that holds it is the primary key. On one database this is easy, because the database counts 1, 2, 3 for you. Spread the writes across many machines and nobody is counting any more.

Then one day two rows get the same id. Nothing crashes. The write succeeds, the API returns 200, and one record quietly overwrites the other. You find out weeks later when a customer says an order disappeared, and by then the evidence is gone. Watch how this design makes one id without ever asking another machine.

Write requestService instancesNTPSnowflake idLast timestamp guardMachine id leaseDatabase

Start up. When the process starts, it leases a machine id, say 7, from ZooKeeper. This is the only time it coordinates with anyone.

This question is small enough to answer completely in twenty minutes, and every part of the answer is a real decision with a real failure attached to it. That is why interviewers like it.

A few words first, because the whole page uses them:

  • A bit is a single 0 or 1. A 64 bit number is the size a database calls bigint.
  • A timestamp here is a count of milliseconds since some starting moment, called the epoch.
  • Sortable means that if you sort ids from small to large, you also get them in roughly the order they were created.

Step 1: Understand the problem#

Every answer here is a number, and every number closes off a design. Ask before you draw a single bit.

You askThey sayWhat it settles
How many ids a second, at peak, across how many hosts?About 10,000 a second across 64 hosts, with room to grow.How big the counter inside each id needs to be. At this rate each host makes well under one id per millisecond, so the counter is there for bursts, not for steady traffic.
Do the ids have to sort by creation time?Roughly. Range scans and time ordered pagination depend on it.A timestamp goes in the front of the id, and it rules out random ids like UUID v4. Say "roughly" out loud, because clocks on different machines never quite agree.
How wide can an id be?64 bits. It has to fit a bigint.The whole budget. Everything from here is dividing 64 bits between time, machine and counter, and every bit one part takes is a bit another part loses.
Are these ids ever visible to users?Yes, in URLs.Two consequences. Ids that count upwards leak how much business you do to anyone who watches, and you cannot use them as a secret. If they had to be unguessable, you would need random bits instead.
Can a generator call another service to get an id?No. This sits inside every write.No central id service, so ids are made inside the application process. Coordination is allowed at startup and nowhere else.
How long does the scheme have to last?Decades. Nobody wants to migrate primary keys.41 bits of milliseconds counted from a custom epoch, not from 1970. Starting the count on deploy day buys back every year since 1970 that you were never going to use.

What you are building#

Functional requirements
What the system does.
  • Generate ids that are unique across the whole fleet. Without asking any other machine on the hot path, the path every write takes.
  • Roughly sortable by creation time. So a range scan and an index on the id both do useful work.
  • 64 bits, so it fits a bigint. Half the storage of a UUID, and it indexes far better.
Non-functional requirements
How well it has to do it. The numbers you commit to.
  • 10,000 ids a second at peak, with headroom.
  • Under 1ms to generate, because this sits inside every write.
  • No single point of failure on the generation path.
  • Ids must not be guessable in sequence if they are ever exposed publicly.
Out of scope
What you cut, and say so out loud.
  • Short human friendly codes. Different problem, covered in the URL shortener page.
  • Random tokens for sessions or password resets. Those need real randomness and must never be time ordered.
  • A strict global order. These ids are roughly ordered, and pretending otherwise causes bugs.

Back of the envelope#

If this kind of estimate is new, the back of the envelope chapter walks through the method. Move the sliders and watch the middle row.

How many ids do you actually need
10 thousand
64
50
Ids per second10,000
Ids per host per millisecond10,000 / 64 / 1000 = 0.16
Milliseconds in that many years2^41
Total ids over the period15.8 trillion
under 1 per ms per host
the counter barely has to work

A millisecond timestamp needs 41 bits to cover 50 years. Everything left over is split between naming the host and counting within a millisecond, and the numbers above show the counter needs very few of those bits.

That middle row is the one people miss. At 10,000 writes a second across 64 hosts, each host makes about 0.16 ids per millisecond. The counter bits exist for bursts, not for steady traffic.

Step 2: Propose the high level design#

The core entities
  • Id64 bits: 41 of milliseconds since a custom epoch, 10 of machine id, 12 of counter. This page has no tables, because the bit layout is the contract.
  • Machine idA number from 0 to 1,023, leased from ZooKeeper at start up and renewed while the process lives.
  • Last timestampThe newest millisecond this process has used. It is the guard that catches a clock going backwards.
Never breaks: Two writes never get the same id, even when a clock jumps or a lease is lost.

Where an id comes from#

Before the bit layout, it helps to see why the obvious answers fail. Each one is a real thing teams ship, and each one breaks at a specific moment.

Every write needs a new id that no other write has ever had. Where does it come from?

  1. First try
    Let the database count
    Use the database auto increment column. Row 1, row 2, row 3. Perfectly unique, perfectly ordered, zero code.
    Where it breaks: The day you split the data across several databases (sharding). Each shard counts on its own, so shard A and shard B both hand out id 1001. The same problem is described in the sharding chapter under what gets hard.
  2. Better
    Random ids (UUID v4)
    Every machine makes 122 random bits. No coordination at all, and a collision is so unlikely you can ignore it.
    Where it breaks: Under heavy writes. Random ids land in random places in the database index, so most inserts touch a page that is not in memory. They are also 128 bits, twice a bigint, in every index and every foreign key. The deep dive below shows why.
  3. What you ship
    Snowflake: time, machine, counter in 64 bits
    Each process builds its own ids from three parts: the current millisecond, its own machine id, and a small counter for ids made within the same millisecond. Unique because no two machines share a machine id. Sortable because time comes first. Twitter published this scheme as Snowflake, and almost everyone copies it.

The first rung is why this question exists at all. Sharding splits one database into many, and the auto increment counter does not survive the split, as the partitioning chapter points out.

Think of shop counters printing receipts. If every receipt number reads "10:42:05, counter 3, receipt 17", no counter ever needs to ask another what number comes next, and two receipts can only clash if two counters both think they are counter 3. Hold on to that last part: it is the weak point of the whole design.

The contract, in bits#

There is no REST API here. The contract is the bit layout, and every other system will depend on it forever, so it goes on the whiteboard first.

Here is one id built by hand, with the standard split of 41 bits of time, 10 bits of machine and 12 bits of counter. Say the time is 1,000 milliseconds after our epoch, the machine is 7, and this is the third id in that millisecond, so the counter is 2.

  • Time goes in the highest bits: 1,000 shifted left by 22 places is 1,000 × 4,194,304 = 4,194,304,000.
  • Machine goes in the middle: 7 shifted left by 12 places is 7 × 4,096 = 28,672.
  • Counter goes at the end: 2.
  • Add them: 4,194,332,674.

Because time sits in the highest bits, any id made one millisecond later is bigger, whatever machine made it. That is all "sortable" means here. Drag the slider to see the trade between machines and counter.

1
41
milliseconds since a custom epoch
10
machine id
12
sequence within the millisecond
10
Years before the timestamp rolls over70 years
Machines you can name1,024
Ids per machine per millisecond4,096
Ids per machine per second4,096,000

The default 10 and 12 split is 1,024 machines and 4,096 ids per machine per millisecond, which is about four million ids a second per host. That is the standard for a reason.

The sign bit stays at zero on purpose. Java's long is signed, and many systems downstream treat the id as a signed number. Setting the top bit would make ids negative, and negative ids break sort order in exactly the place you relied on it.

The epoch is custom, not 1970. With a 2010 epoch, 41 bits of milliseconds lasts until about 2079. Counting from 1970, more than half of those years would already be spent.

The whole system on one whiteboard#

Figure 1 is the same path as the picture at the top, with the alarms added.

Figure 1. Every box that could add latency to a write is missing on purpose. The lease is touched at startup and on renewal, the clock is read locally, and nothing crosses the network to make an id.

Walking Figure 1:

  1. A write arrives at any instance. There are 64 of them and they do not talk to each other. Stateless means an instance keeps nothing another instance would need, so any of them can take any request.
  2. The instance asks its own in-process generator for an id. This is a function call, not a request over the network.
  3. The generator reads the clock and compares it with the last timestamp it issued. If time has gone backwards it refuses. That is the first deep dive.
  4. The id goes into the insert. Total added latency is a few hundred nanoseconds.

The two dashed arrows are the only coordination in the design. A machine id is leased at startup and renewed in the background, and NTP (network time protocol, the service that keeps a machine's clock in line with real time) keeps the clock honest. Both sit off the write path, and both are where this design goes wrong when it goes wrong.

There is no id service in Figure 1, and that is worth saying out loud. A central id service would put a network call inside every write, and it would need to be more available than the database it feeds.

Your answer

Two things in Figure 1 can hand out a duplicate id without raising a single error. Which two, and what would you do about each?

Step 3: Design deep dive#

The clock is not a monotonic counter#

Monotonic means "only ever goes up". Snowflake swaps coordination for trust in the local clock: time only moves forward, so ids only move forward. Except a machine's clock does not only move forward. It drifts, and when NTP corrects it, the correction can jump it backwards. Time and clocks shows how far off a clock can be. Step through what that does to an id.

1/5 A request arrives and the generator reads the clock. It gets 1,700,000,050 milliseconds.

What should the generator do about a clock that can jump backwards?

  1. First try
    Trust the clock
    Read the time, build the id, move on. It is what most first versions do.
    Where it breaks: The first time NTP steps the clock back past a millisecond already used. The generator repeats a timestamp with the same machine id and the counter restarts at 0, so it issues an id it has issued before, and nothing reports an error.
  2. Better
    Make NTP slew instead of step
    Stepping means jumping the clock straight to the right time. Slewing means running the clock very slightly slow until true time catches up, so it never goes backwards during a correction.
    Where it breaks: Rarely, but it still happens. A virtual machine paused while it moves hosts can wake up far off, and one misconfigured server is enough. Prevention alone is half an answer.
  3. What you ship
    Slew, and remember the last timestamp
    Keep the last timestamp you issued. If the clock now reads earlier than that, do not generate. If the gap is a few milliseconds, wait until the clock passes it. If the gap is large, fail the request loudly and raise an alarm. Never generate anyway and hope.

Snowflake implementations that quietly carry on are the ones that produce the duplicate nobody notices for a month. Name both defences in the interview: slew to make it rare, and the guard to make it safe.

What is actually wrong with a UUID#

UUID v4 is 128 bits, 122 of them random. Collisions are genuinely not the problem: you would need to generate billions of them before one becomes likely. The problem is what random ids do to the database.

A database index on the id works like a phone book kept in sorted order. In most databases it is a B-tree: sorted pages of keys, with the busy pages kept in memory and the rest on disk. (Indexes and storage engines explain it from the start.)

  • Time ordered ids are like new names that always sort to the end of the phone book. Every new row lands on the last page, which is already in memory, next to the row inserted a millisecond ago.
  • Random ids are like new names that land anywhere. Every new row goes to a random page, which usually has to be read from disk, changed, and written back. At high write rates the same insert rate causes far more disk work, the index fragments, and the part of it you use stops fitting in memory.

There is also the width. A UUID stored as text is 36 bytes, or 16 as raw binary, against 8 for a bigint, and that cost repeats in every secondary index and every foreign key.

UUID v7 fixes the ordering by putting a millisecond timestamp in its first bits, which makes it a reasonable choice when you want a standard format and do not want to run a machine id lease. It is still 128 bits.

Where the machine id comes from#

The whole scheme is only unique if no two live processes hold the same machine id. Remember the receipt counters: two counters both calling themselves counter 3 is the only way receipts clash. Getting this right is not free.

How does a freshly started process learn its machine id?

  1. First try
    Hardcode it in config
    Each server gets a number in its config file. Simple and easy to read.
    Where it breaks: The first time someone copies a deployment, or autoscaling starts a new container from the same image. Two processes now share a number.
  2. Better
    Derive it from the IP address
    Take the last bits of the machine IP address. No config to maintain.
    Where it breaks: When addresses get reused. A container dies, a new one gets its IP a minute later, and for a while two processes can believe they own the same id.
  3. What you ship
    Lease it from ZooKeeper or etcd
    At startup, take a numbered lease from a coordination service and keep renewing it in the background. A lease is a lock that expires on its own, so a dead host gives its number back only after its lease runs out. If the process cannot renew its lease, it stops making ids at once.
Where the lease usually lives
  • etcdkey value store with leases built in
  • ZoZooKeepercoordination service, ephemeral nodes

The last sentence of that rung is the one people forget. A lease can expire during a network partition while the process keeps running and thinks it is fine. If it keeps generating, and the number has gone to someone else, both make identical ids. How leases expire, and why the holder does not know it lost one, is in leader election and locks.

Break it#

The dial here is not traffic. It is the health of the two things this design cannot avoid: the clock and the lease.

Clock and lease health
healthy
healthyburstclock skewclock steps backlease lost
Healthy. NTP is slewing, the lease is renewed, ids increase. Nothing in this picture makes a network call to generate an id.
Read every step as text
  1. healthy · Healthy. NTP is slewing, the lease is renewed, ids increase. Nothing in this picture makes a network call to generate an id.
  2. burst · Degraded. A batch import asks for more than 4,096 ids inside one millisecond. The counter is used up, so the generator waits until the clock ticks. Correct behaviour, but it shows up as a latency spike and is worth an alert.
  3. clock skew · Degraded. This host is 30ms ahead of the rest of the fleet. Ids from here sort slightly ahead of ids made later elsewhere. Nothing is broken, but "roughly sortable" now means roughly, and any logic that assumes a strict order across hosts is already wrong.
  4. clock steps back · Down. The clock moved backwards past a timestamp already used. A generator that keeps going emits a duplicate id. A generator that remembers its last timestamp refuses, and either waits or errors, which is the only safe answer.
  5. lease lost · Down. The lease expired during a network partition and another host was handed machine id 7. Two live processes now share a machine id, so they produce identical ids within the same millisecond. The process must stop generating the moment it cannot confirm its lease.

Both red states produce duplicate ids, and neither produces an error by itself. That is what makes this design worth being careful with: it fails silently, and in both cases the safe move is to stop, not to continue.

Trade-offs#

ChoiceWhat you gainWhat you payPick it when
Snowflake style 64 bit idsNo coordination on the write path, sortable by time, half the width of a UUID and far better index locality.A machine id to hand out safely, and a hard dependency on a well behaved clock.High write volume where the id is a primary key and ordering earns its keep.
UUID v4Zero infrastructure. Generate anywhere, including on the client, with no coordination of any kind.128 bits, and random insert order that fragments B-tree indexes and pushes the working set out of memory.Low write volume, ids created offline on a device, or when the id is not the clustered key.
UUID v7A standard format with a timestamp first, so it sorts and indexes almost as well as Snowflake, with no machine id to lease.Still 128 bits, and it still leaks creation time.You want time ordering without running a coordination service. Often the pragmatic middle.
Database ticket serverTrivially correct and strictly increasing, with no clock involved.A network round trip per id and a single point of failure, unless you hand out ranges, which brings back gaps.Modest scale where correctness matters more than throughput, or as the source behind a range allocator.

The level bar#

One moment from this round, answered three ways. Pick the one you think is staff level before the labels appear. (What changes at senior and staff explains the gap.)

Pick the staff level answer

InterviewerA batch import asks one host for 50,000 ids inside one millisecond. What happens?

Show which is which without guessing
  • Answer A: Senior. Knows the limit and the correct behaviour. Says nothing about how anyone would notice.
  • Answer B: Staff. Accepts the correct behaviour, tells a burst apart from a capacity problem, and moves the noisy caller out of the way.
  • Answer C: Mid level. Changes the layout every id ever issued depends on, to fix a burst that lasts a few milliseconds.

Then run the question cold, with a 45 minute timer and no notes, and score the run on the five things this question is really testing.

Score your last run
0 / 10
  1. Why not a database sequence
  2. The bit layout
  3. The clock
  4. Machine ids
  5. Against UUIDs
Pick 0, 1 or 2 on every row. Be the strict interviewer, not the kind friend.

Interview replay#

Interviewer
You need unique ids across a hundred service instances. What do you use?
Deliberately open. The first sentence tells them how you think.
You
A Snowflake style 64 bit id: timestamp in the high bits, then a machine id, then a per millisecond counter. The reason over UUIDs is index locality. Random ids scatter B-tree inserts across the whole index, and at high write rates that costs far more in disk activity than making the id ever will.
Answers with the database consequence, not the collision math. That is the reason that actually drives the decision.
Interviewer
Where does the machine id come from?
The weak point of the scheme, and they know it.
You
A lease from ZooKeeper or etcd at startup, renewed in the background. Config files break the first time a deployment is cloned, and the IP breaks when addresses are reused. What matters most is what happens when the lease cannot be renewed: the process stops making ids, because if another host has taken that number, both are making the same ids and nothing will tell you.
Goes straight to the failure case without being asked.
Interviewer
What if the clock goes backwards?
The standard follow up. There is a correct answer.
You
Track the last timestamp issued. If the clock reads earlier than that, refuse: wait for the clock to catch up if the gap is a few milliseconds, or fail the request if it is large. I would also set NTP to slew instead of step so this is rare. The one thing that must not happen is generating anyway, because that makes a duplicate primary key with no error attached.
Names both the prevention and the handling. Doing only one of the two is the common half answer.
Interviewer
These ids are in our public URLs. Any concern?
A different axis entirely, and it catches people who have only memorised the bit layout.
You
Two. They leak creation time, since the timestamp is right there in the high bits. And they are close to sequential, so anyone can estimate how many records you create per day and try nearby ids. If either matters I would keep the Snowflake id inside and show a separate opaque id outside, rather than make one id do both jobs.
Separating the internal key from the external identifier is the senior answer, and it is one sentence.

Checkpoint#

Checkpoint

1. What is the strongest argument against UUID v4 as a primary key at high write volume?

2. The machine id lease expires during a network partition while the process keeps serving traffic. What must the process do?

3. You move machine id from 10 bits to 14 bits, keeping the id at 64 bits. What did you just give up?

Worth memorising
  • 64 bits: 1 sign, 41 time, 10 machine, 12 counter. The Snowflake split everyone copies.
  • 41 bits of milliseconds is 69 years from your own epoch, which is why you never start at 1970.
  • 1,024 machines, 4,096 ids per machine per millisecond. About four million a second per host.
  • 10,000 ids a second across 64 hosts is under one per millisecond each. The counter bits are for bursts, not steady traffic.
Say this in 60 seconds

I would use a Snowflake style 64 bit id: a millisecond timestamp from a custom epoch in the high bits, then a machine id, then a counter for ids made within the same millisecond. It is generated in process with no network call, so nothing new sits inside every write, and because it is time ordered, inserts land at the right edge of the index instead of scattering across it the way UUID v4 does. The machine id comes from a lease in ZooKeeper or etcd, and the process must stop generating if it cannot renew that lease. The one real risk is the clock: I would set NTP to slew rather than step, track the last timestamp issued, and refuse to generate if the clock ever reads earlier than that, because the failure here is a duplicate primary key with no error next to it.