Notes
Clock drift
Every server's clock drifts, by tens of milliseconds a day. NTP keeps correcting them, usually to within a few milliseconds, but on a bad network it can be a hundred or more, and a correction can even jump the clock backwards. So two servers can disagree about the time, and you don't know by how much.
- quartz drifts ~ tens of ms
- per day
- NTP corrects it, usually to
- within a few ms… or 100+ ms
- on a bad network
- NTP can jump the clock
- BACKWARDS
- leap seconds, VM pauses
Last write wins
Here's the damage. Server B, whose clock runs fast, writes version one. Moments later server A writes version two, but A's timestamp is earlier. A database using last-write-wins by timestamp keeps B's write and silently throws away the newer edit. No error, no log line. Cassandra's default conflict rule works this way.
real order: B writes v1, then A writes v2 timestamps: B: 12:00:00.180 (fast clock) A: 12:00:00.050 last-write-wins keeps .180 → v1 wins, v2 is lost no error, no log line
Logical clocks
The fix is to stop trusting wall clocks for ordering and count events instead. A Lamport clock is a counter on each node. Increment it on every event, attach it to every message, and on receiving, jump to the larger value plus one. Now if event A caused event B, A's number is smaller. Order follows cause and effect, not drifting quartz.
each node keeps a counter local event: c = c + 1 send message: attach c receive m: c = max(c, m) + 1 if A caused B, then L(A) < L(B) gives an order consistent with cause and effect
Vector clocks
Lamport clocks give an order, but can't tell you whether two edits were truly concurrent. Vector clocks can. Each version carries one counter per node. If every counter in one version is at least the other's, it came after. If neither dominates, the edits were concurrent: a real conflict, so keep both and merge or ask the user. Dynamo-style systems and Riak use this.
one counter PER NODE edit 1: [A:1, B:0] edit 2: [A:1, B:1] ← saw edit 1 edit 3: [A:2, B:0] ← did not edit 2 vs edit 3: neither ≥ the other → CONCURRENT: a real conflict keep both, merge or ask user
TrueTime
Some systems fix the clocks instead. Google Spanner's TrueTime uses GPS and atomic clocks to know the time within a few milliseconds of uncertainty, and makes each commit wait out that uncertainty. CockroachDB uses hybrid logical clocks: wall time plus a logical counter, close to real time but safe for ordering.
- Google Spanner TrueTime:
- GPS + atomic clocks give
- time ± a few ms
- commit WAITS out the
- uncertainty → safe ordering
- hybrid logical clocks (HLC):
- wall time + a logical counter
- (CockroachDB, YugabyteDB)
Unique IDs
Now the related problem: IDs. With one database, auto-increment works. With a thousand servers and sharded databases, you want IDs that are unique without a round trip to one central counter. Ideally they're also roughly sortable by time, so indexes stay efficient, and fit in sixty-four bits.
- unique across all servers
- generated without a round
- trip to one database
- nice to have:
- • roughly time-sortable
- (good index locality)
- • 64-bit (fits a BIGINT)
- • not guessable, if public
Options
The options. Auto-increment: simple, but one writer, guessable, and awkward with sharding. Random UUIDs: no coordination at all, but a hundred and twenty-eight random bits make B-tree inserts land everywhere, which hurts. ULID and UUID version seven put a timestamp first, so they sort by time. And Snowflake IDs fit it all into sixty-four bits.
- auto-increment: simple, one
- writer, guessable, hard to shard
- UUIDv4: 128 random bits, no
- coordination, but random order
- → slow B-tree inserts
- ULID / UUIDv7: timestamp first,
- random after → sortable
- Snowflake: 64-bit, sortable
Snowflake
Snowflake, from Twitter, packs forty-one bits of milliseconds since a chosen epoch, ten bits of machine id and a twelve-bit sequence. That's sixty-nine years, a thousand machines, and four thousand IDs per millisecond each, roughly time-sorted with no coordination. One rule: if the clock goes backwards, the generator must wait, or it could repeat IDs.
| 41 bits | 10 bits | 12 bits | | ms since | machine | sequence| | epoch | id | per ms | ~69 years of timestamps 1024 machines 4096 IDs per ms per machine sorted by time, no coordination clock goes back? wait or refuse
Say it like this
In the interview: never order events across servers by wall-clock time. Use version numbers, Lamport clocks, or vector clocks when you need to detect conflicts. For IDs at scale, propose Snowflake or UUID version seven, and explain why a random UUID makes a poor primary key on a large B-tree table.
- ordering across servers:
- don’t trust wall clocks;
- version numbers, Lamport or
- vector clocks for conflicts
- unique IDs at scale:
- Snowflake (64-bit, sortable)
- or UUIDv7 / ULID
- not UUIDv4 as a primary key
- on a big B-tree table