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.
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.
Every answer here is a number, and every number closes off a design. Ask before you draw a
single bit.
You ask
They say
What 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.
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.
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.
10:42:05counter 1receipt 1710:42:05counter 2receipt 1710:42:05counter 3receipt 1710:42:05counter 3receipt 17no counter asks anothera second counter 3:the same receipt
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.
counting from 1970: ends 2039your own epoch, 2010: ends 2079todaymore than half spent41 bits of milliseconds is 69 years
Figure 1 is the same path as the picture at the top, with the alarms added.
1next id2is time moving forward3insert4machine id, at startupdisciplinerefusalsWrite requestService instancex64, statelessNTPslew, never stepSnowflaketime | machine | seqLast timestampguardMachine id leaseZooKeeper, renewedAlarmsskew, refusalsDatabasebigint primary key
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:
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.
The instance asks its own in-process generator for an id. This is a function call, not a
request over the network.
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.
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.
a function call64 instances,none of them talka central id serviceid servicea network call in every write
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?
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.
GeneratorSystem clockDatabasenow()insert id @ ...050NTP steps back 30msnow()insert duplicate 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.
1000100110021003an NTP correction jumps backthe same id againthe guard refuses until time passes 1002
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.
time ordered idsrandom idsevery insert lands on thelast page, already in memoryeach insert lands anywhere,often read from disk
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.
bigint (Snowflake): 8 bytesUUID as raw binary: 16 bytesUUID as text: 36 bytespaid again in every index and 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.
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.
lease: machine 7old process, cut off,still machine 7new process,given machine 7same idsit fails silently: the safe move is to stop
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
now()insertGeneratormachine id 7System clockMachine idleaseDatabase
now()insertGeneratormachine id 7System clockMachine idleaseDatabase
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
healthy · Healthy. NTP is slewing, the lease is renewed, ids increase. Nothing in this picture makes a network call to generate an id.
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.
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.
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.
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.
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
Why not a database sequence
The bit layout
The clock
Machine ids
Against UUIDs
Pick 0, 1 or 2 on every row. Be the strict interviewer, not the kind friend.
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.
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.