Skip to the chapter
←System DesignGGroundworkSDSystem Design
System Design/Advanced/A5
25 / 36 written chapters
/

The cover
  1. B1The mental model
  2. B2Client-server basics
  3. B3Vertical vs horizontal
  4. B4Databases in design
  5. B5Caching fundamentals
  6. B6APIs & communication
  7. B7Walkthrough: URL shortener
  1. I1Load balancing in depth
  2. I2CDNs
  3. I3Message queues & async
  4. I4Consistency models
  5. I5Replication
  6. I6Rate limiting & throttling
  7. I7Designing for availability
  8. I8Storage systems
  9. I9Search systems
  10. I10Microservices
  11. I11Security basics
  12. I12Walkthrough: feed/chat
  13. I13Walkthrough: notifications
  1. A1CAP theorem in depth
  2. A2Transactions and isolation
  3. A3Storage engines
  4. A4Sharding at scale
  5. A5Time, ordering and IDs
  6. A6Distributed consensus
  7. A7Batch & stream processing
  8. A8Fault tolerance
  9. A9Observability at scale
  10. A10Release safety
  11. A11Capacity estimation
  12. A12Case studies
  13. A13Walkthrough: key-value store
  14. A14Walkthrough: web crawler
  15. A15Walkthrough: payments
  16. A16Tradeoff thinking

SDA5 · Part 3 · Advanced

Time, ordering and IDs

Machines disagree about what time it is, so a distributed design has to decide what to trust: the wall clock, a counter that only counts causes, or a clock that admits its own error.

22 min read3 diagrams9 sections0 exercises
25 of 36

In this chapter

  • Two questions that sound like one
  • Two clocks in every computer, for different jobs
  • How wrong can a clock be?
  • Logical time: order without clocks
  • Hybrid logical clocks: timestamps that look like time and respect causes

and 4 more

about 22 min
09On this page
  1. Two questions that sound like one
  2. Two clocks in every computer, for different jobs
  3. How wrong can a clock be?
  4. Logical time: order without clocks
  5. Hybrid logical clocks: timestamps that look like time and respect causes
  6. TrueTime: a clock that reports its own error
  7. Generating IDs: uniqueness, order and no coordination
  8. Choosing
  9. Recognizing it in an unseen problem

Two questions that sound like one

Almost every distributed design silently asks one of two questions. The first is what time is it?, wanted for expiry, rate limits, logs and "created at". The second is what happened before what?, wanted for conflict resolution, replication order and transactions. On one machine the answers coincide: read the clock, and the later number happened later. Across machines they separate, because each machine has its own clock, and no message can tell one machine what another's clock says right now. The replication chapter's dry run already showed the cost: a write that really came 20 ms later lost a last-write-wins comparison because its node's clock ran 80 ms slow. This chapter is about that failure and its four remedies: sturdier use of physical clocks, logical clocks, hybrid clocks, and clocks that publish an error bar. It ends with the practical problem that depends on all of them, generating identifiers.

Two clocks in every computer, for different jobs

Operating systems expose two kinds of clock, and mixing them up is a classic bug. On Linux the real-time clock (CLOCK_REALTIME) measures wall-clock time. It is settable, and the man page says it is affected by discontinuous jumps in the system time and by the incremental adjustments of NTP. The monotonic clock (CLOCK_MONOTONIC) is not settable and is not affected by discontinuous jumps, though it is still slewed (sped up or slowed down slightly) by NTP, and it does not count time while the system is suspended. Use the monotonic clock to measure a duration on one machine: a timeout, a latency, the age of a cache entry. Use the real-time clock only when you need a calendar time, and never assume two machines' real-time clocks can be compared exactly.

⚠ A one-second step made a DNS resolver panic for about 6 hours 45 minutes At the end of 31 December 2016 a leap second was added. Cloudflare's DNS software, written in Go, measured resolver round-trip times by subtracting two wall-clock readings. When the clock stepped back, some differences came out negative, the smoothed value went negative, and a random-number call that panics on negative input was reached. Only customers using CNAME records were affected, and at the peak about 0.2% of DNS queries were, between 00:00 and 06:45 UTC; the worst-hit machines were patched within 90 minutes. The root cause was the assumption that time cannot go backwards. (Go has tracked monotonic time inside time.Time values since version 1.9, so this exact bug cannot recur there, but the lesson applies to any language.)

How wrong can a clock be?

Quartz oscillators drift. Google's Spanner paper applies a drift rate of 200 microseconds per second as its conservative worst-case bound for a server clock, which is not a small number:

Time since last synchronisationArithmetic at 200 µs per secondPossible error
30 seconds (Spanner's poll interval)30 × 200 µs6 ms
1 hour3,600 × 200 µs0.72 s
1 day86,400 × 200 µs17.28 s

NTP keeps drift in check by polling servers and slewing the clock, and it works well, but not perfectly. RFC 5905 (NTPv4) says that typical primary servers are precise within a few tens of microseconds, typical clients on fast LANs within a few hundred microseconds, and, with poll intervals up to 36 hours, within a few tens of milliseconds. A cloud provider's own time service narrows this: Amazon runs the Amazon Time Sync Service from satellite-connected and atomic reference clocks in each Region, and it offers ClockBound, a daemon and library that reports the instance's clock error so you can check whether a timestamp is in the past or future relative to your clock. Across a fleet you should still budget for tens of milliseconds of disagreement in the worst case, and for rare, much larger excursions when a time source fails or a VM is paused and resumed.

Leap seconds are a special hazard because UTC inserts an extra second and software must decide what the clock shows. The NTP protocol has a two-bit leap indicator warning that the last minute of the day will have 61 or 59 seconds. Google avoids the step by smearing: it spreads the extra second linearly over 24 hours, noon to noon UTC, so smeared time is just under 0.5 s behind UTC when the leap second begins and just under 0.5 s ahead when it ends, agreeing with UTC again at both ends of the window, with a frequency change of about 11.6 ppm (1 second in 86,400 is 11.57 parts per million). The consequence is that two machines can legally disagree by up to about half a second around a leap event if one smears and the other does not, which is why Amazon cautions against mixing smeared and non-smeared sources in one time client. The most recent leap second was at the end of 2016, and a 2022 resolution of the General Conference on Weights and Measures commits to abandoning them by or before 2035, as Amazon's documentation notes, but old data, old software and replayed history still carry the hazard.

Process pauses: the clock is right but the program is not looking

Even a perfect clock does not help if the program stops looking at it. A garbage-collection pause, a VM migration, a laptop lid or a page fault storm can freeze a process between "check that my lease is valid" and "act on it". When it resumes, the lease may have expired and another node may hold the lock, yet this process still believes it is the leader. The consensus chapter explains the cure, the fencing token, which moves the check to the resource. The lesson for time is the same: never base a safety property on "no more than X milliseconds can pass between these two lines".

Logical time: order without clocks

Lamport's 1978 paper begins from a different question. Instead of asking when an event happened, ask whether it could have influenced another. He defines happened-before: within one process, earlier events precede later ones; sending a message precedes receiving it; and the relation is transitive. Two events related by neither are concurrent. This is a partial order, not a timeline: it says only what could have caused what.

A Lamport clock is a counter per process that obeys the paper's Clock Condition: if event a happened before b, then C(a) < C(b). The rules are short: increment before each event; stamp every message with the counter; on receipt set the counter to the larger of its own and the message's, plus one. Breaking ties by process id turns the partial order into a total order that every node can compute alone. The catch is in the paper's own note: the converse does not hold. C(a) < C(b) does not mean a happened before b, so a Lamport clock cannot tell a cause from a coincidence.

A vector clock keeps one counter per process instead of one in total. Each process increments its own slot on every event and, on receipt, takes the element-wise maximum with the message's vector. Compare two vectors: if every slot of V1 is less than or equal to V2's, (and the two are not identical) then V1 happened before V2; if each is larger somewhere, the events are concurrent. That recovers exactly what the Lamport clock loses. The price is a vector the size of the number of writers. The Dynamo paper used vector clocks to detect conflicting versions of a key and returned both to the application, and it truncated the clock when it grew past a threshold (its example is 10 pairs), accepting that the ancestry check can blur.

P1 P2 P3 a send m1 recv m1 send m2 c recv m2 L1 [1,0,0] L2 [2,0,0] L3 [2,1,0] L4 [2,2,0] L1 [0,0,1] L5 [2,2,2] c has Lamport 1, below "send m1" at 2, yet the two are concurrent.
The Lamport counter orders c before send m1 although neither could have influenced the other. The vectors [0,0,1] and [2,0,0] each exceed the other somewhere, which reveals the concurrency.

Dry run: the six events in the figure

EventLamport ruleLamportVector ruleVector
a: P1 local0 + 11P1 slot +1[1,0,0]
P1 sends m1 to P21 + 12P1 slot +1[2,0,0]
c: P3 local0 + 11P3 slot +1[0,0,1]
P2 receives m1max(0, 2) + 13P2 slot +1 gives [0,1,0]; element-wise max with [2,0,0][2,1,0]
P2 sends m2 to P33 + 14P2 slot +1[2,2,0]
P3 receives m2max(1, 4) + 15P3 slot +1 gives [0,0,2]; element-wise max with [2,2,0][2,2,2]
Is a before "P3 receives m2"?1 < 5, consistent but not proof—[1,0,0] ≤ [2,2,2] in every slot: yesProven
Are c and "P1 sends m1" ordered?1 < 2 suggests so—[0,0,1] vs [2,0,0]: each is larger in some slot: concurrentConcurrent

Hybrid logical clocks: timestamps that look like time and respect causes

Lamport and vector clocks cannot answer "what was the state at 14:00?", because their numbers are unrelated to the wall clock. Physical clocks answer that but can invert causes. The hybrid logical clock (HLC), proposed by Kulkarni and colleagues, keeps a pair (l, c): l is the largest physical time the node has seen, from its own clock or from any message, and c is a counter that breaks ties when l does not move. It fits in 64 bits, stays close to the physical clock, and is built to coexist with NTP. The paper's rules are:

  • Local or send event: set l to the larger of the old l and the physical time. If l did not change, increment c; otherwise reset c to 0.
  • Receive event: set l to the largest of the old l, the message's l and the physical time. If l equals both the old l and the message's l, c is the larger of the two c values plus one; if it equals only the old l, increment c; if it equals only the message's l, take the message's c plus one; otherwise reset c to 0.

Compare timestamps as pairs, l first, then c. The effect is that a node whose clock is slow never produces a timestamp lower than something it has already heard about.

Dry run: the slow clock, when the later write is causally after

This is a variant of the replication chapter's scenario in which B has already received A's update before it writes, so B's write is causally later and its stamp is 9.980 (true time 10.060) instead of the concurrent 9.940.

StepNode, physical timePrior (l, c)Rule appliedNew (l, c)
1. A writesA, 10.000(0, 0)l = max(0, 10.000) = 10.000, changed, so c = 0(10.000, 0)
2. B receives A's update (B's clock is 80 ms slow)B, 9.960(9.900, 0)l = max(9.900, 10.000, 9.960) = 10.000, equal to the message's l only, so c = 0 + 1(10.000, 1)
3. B writes, causally after AB, 9.980(10.000, 1)l = max(10.000, 9.980) = 10.000, unchanged, so c = 1 + 1(10.000, 2)
4. Compare the two writes——(10.000, 2) beats (10.000, 0)B's later write wins; its raw wall time, 9.980, would have lost
5. B's clock moves onB, 10.010(10.000, 2)l = max(10.000, 10.010) = 10.010, changed, so c = 0(10.010, 0)

HLC fixes a causally later write receiving an earlier stamp. It does not fix truly concurrent writes, as in the replication chapter, where B never saw A's write: one of the two still has to lose or be merged. CockroachDB is a real user. Its documentation describes each node's HLC as a physical part always close to local wall time plus a logical part for ties, and warns that clock skew beyond the configured maximum offset can violate single-key linearizability between causally dependent transactions (serializability at SERIALIZABLE isolation holds regardless). A node whose clock is out of sync with at least half of the others by 80% of the maximum offset shuts itself down, so a bad clock cannot silently corrupt the order.

TrueTime: a clock that reports its own error

Spanner takes the opposite route to logical clocks: make physical time trustworthy enough, and expose its uncertainty. TrueTime's TT.now() returns an interval, [earliest, latest], that is guaranteed to contain the true time. Half the width is the error bound ε, so the interval is 2ε wide. Google's time masters use GPS receivers and atomic clocks (it calls the atomic-clock masters "Armageddon masters"), and each machine's daemon polls several of them, rejects liars, and advertises a slowly growing uncertainty between polls. In the paper, ε is typically a sawtooth varying from about 1 to 7 ms over each 30-second poll interval, which is 4 ms most of the time. The arithmetic is visible: 30 s × 200 µs/s = 6 ms of drift allowance, which accounts for the sawtooth between 0 and 6 ms, and the remaining 1 ms is communication delay to the time masters.

interval, width 2ε commit wait TT.now().earliest s = latest TT.after(s) true release locks, reply to client choose s as the commit timestamp Wait until s is surely in the past everywhere, then let anyone see the commit. The wait costs milliseconds, not round trips; the paper measured about 5 ms of commit wait.
External consistency by waiting: if the next transaction starts after this one is acknowledged, its timestamp is guaranteed to be larger.

The protocol follows. A read-write transaction picks a commit timestamp s no earlier than TT.now().latest, then the coordinator waits until TT.after(s) is true before applying the commit and replying. By then s is in the past on every machine, so any transaction that starts after the client hears "committed" gets a larger timestamp. That is Spanner's external consistency, the strict serializability described in the consistency models chapter. The expected wait is at least 2ε, and the paper says it is typically overlapped with the Paxos communication; in its one-replica experiments commit wait was about 5 ms and Paxos latency about 9 ms. It also explains why TrueTime matters for the engineering trade: Spanner pays with specialised time hardware; CockroachDB, running on ordinary NTP clocks, accepts a configured maximum offset and restarts reads that fall inside an uncertainty interval, and its documentation says that skew beyond the bound can break single-key linearizability between causally dependent transactions.

Generating IDs: uniqueness, order and no coordination

Every row, message and event needs an identifier, and generating one is a time-and-ordering problem in disguise: you want values that are unique without a round trip to a central allocator, roughly sortable by creation time so indexes stay cheap, and compact. No scheme gets all three for free.

SchemeHow it worksStrengthsCosts
Database sequence or auto-incrementOne counter in one database hands out the next integerCompact, strictly increasing from that counter, trivially uniqueSingle point of contention and failure. PostgreSQL's manual says a value obtained is not reclaimed if the transaction aborts, so aborts and crashes leave gaps and sequences cannot be gapless. Reveals volume
Ticket server (Flickr)Dedicated MySQL tables with an auto-increment column; REPLACE INTO then LAST_INSERT_ID(). Two servers split odd and even numbers (auto-increment-increment = 2) and clients round-robinSimple, compact, survives one server downAn extra network hop per ID, a service to run, and the two servers' numbers drift slightly apart, so order across them is approximate
UUIDv4122 random bits in a 128-bit value (RFC 9562)No coordination at allNo order, 16 bytes, and random insertion points give poor index locality. The RFC notes the effect can be an order of magnitude or more against time-ordered values
UUIDv748-bit Unix millisecond timestamp, then version, 12 bits and 62 bits of random or counter data, with variant bits (74 usable bits)Sortable, standard, index-friendly. PostgreSQL's documentation (version 18 and later) lists a uuidv7() function, which uses part of the 12-bit field for a sub-millisecond timestampExposes creation time. The RFC says not to assume UUIDs are hard to guess, and recommends v4 where unpredictability matters
ULID48-bit millisecond timestamp plus 80 random bits, written as 26 Crockford base32 characters; within one millisecond the random part is incremented to stay monotonicSortable as a string, shorter text than the 36-character UUID formSame timestamp leak; a community specification rather than an IETF standard
Snowflake-styleTwitter's layout: 41-bit millisecond timestamp from a custom epoch, 10-bit machine id, 12-bit sequence, in 63 bits64-bit integers, time-sorted, no per-ID coordinationNeeds a unique machine id per generator, and depends on the clock never going back
Snowflake, 63 bits 41 time 10 machine 12 sequence UUIDv7, 128 bits 48 time 12 62 random or counter ULID, 128 bits 48 time 80 random Not to a common scale. The small UUIDv7 boxes are the 4 version and 2 variant bits.
The time field sits first in all three, so sorting by value sorts by creation time. What fills the rest is where they differ.

Dry run: what the bit counts allow

QuestionArithmeticResult
How long does a 41-bit millisecond timestamp last?241 = 2,199,023,255,552 ms ÷ (1,000 × 86,400 × 365.25)about 69.7 years, which matches the "69 years" in Snowflake's description
How many machines does a 10-bit id allow?2101,024
How many IDs per millisecond per machine?212, the sequence "rolls over every 4096"4,096, or 4,096,000 a second per machine
Fleet ceiling with 1,024 machines4,096,000 × 1,0244,194,304,000 a second, far above any real demand
Chance that a trillion UUIDv4s contain a duplicaten(n−1) ÷ (2 × 2122) with n = 1012about 9.4 × 10−14, negligible
IDs before a UUIDv4 collision is as likely as notabout 1.18 × √(2122)about 2.7 × 1018
Random bits per millisecond in the RFC's pure-random UUIDv712 + 6274 bits, so collisions inside one millisecond need around 237 IDs in that same millisecond

The hazards, which are all clock hazards

  • The clock goes backwards. A time-based ID generator that sees an earlier clock reading may reuse a timestamp and sequence, producing a duplicate. The archived Snowflake design refuses to generate IDs until the clock passes the last time it used. Do the same: stall, or fail loudly, never reuse.
  • Two generators share a machine id. Snowflake-style schemes assume each worker id is unique. Assign ids from a coordination service with a lease, or from a fixed configuration, not from hostnames that can repeat.
  • Order is approximate across machines. IDs from different machines are sorted only to within their clock skew. If two events must be ordered exactly, use a per-entity sequence or a log, not the timestamp inside an ID. This is the same limit as last-write-wins.
  • IDs leak information. A sequence reveals volume, a time-based ID reveals creation time. Do not use either as a secret or an access token.
  • Index locality. Random primary keys cause random page inserts in a B-tree, as the storage engines chapter describes; time-prefixed IDs append at the right-hand edge. Conversely, a monotonically increasing key makes all inserts hit one shard in a range-partitioned table, a hot-spot the sharding chapter covers.

Choosing

NeedReach forBecause
A duration, timeout or latency on one machineThe monotonic clockNot stepped by NTP jumps; check how it treats suspend (Linux has CLOCK_BOOTTIME for that)
Calendar time for display or coarse expiryThe wall clock, with NTP, and slack in the comparisonClocks disagree by milliseconds to seconds
Decide which of two writes is later, across nodesPer-entity version numbers, a hybrid logical clock, or a consensus log, not raw wall timeWall-clock last-write-wins loses causally later writes
Detect that two versions are concurrentVersion vectors (or vector clocks), accepting the metadata costOnly they distinguish "after" from "concurrent"
Strict global transaction orderSpanner-style bounded clocks with commit wait, or a single sequencerPay a few milliseconds, or one hot component
Primary keys for a sharded OLTP tableUUIDv7 or Snowflake-style 64-bit IDsUnique without coordination, mostly time-sorted, index friendly
Unguessable public identifiersUUIDv4, or random tokens, kept apart from the primary keyNo embedded time or counter
Say it like this → "I won't compare wall-clock times from two machines to decide which write is newer. NTP keeps them within milliseconds on a good day, but a slow clock makes a later write look earlier, and a pause or leap second can make it worse. I'll attach a version to each record and, where I need physical-looking timestamps, use a hybrid logical clock, so a node that has seen a write never stamps anything earlier. For IDs I'd use time-prefixed 64-bit IDs, Snowflake-style, with a leased worker id and a guard that refuses to issue IDs if the clock moves backwards, so primary keys are unique, index-friendly and need no central allocator."

Recognizing it in an unseen problem

  • Signals: "last write wins", "order of events", "timestamps", "unique ID", "expires at", "leader lease", "audit log". Each needs you to say whose clock and how wrong it can be.
  • The naive design uses now() on each server for ordering. Ask yourself what happens when one server's clock is 80 ms slow, or steps back a second.
  • Quantify the error budget. Drift of 200 µs a second is 6 ms per 30 seconds and 17 seconds a day without correction; NTP over the Internet gives tens of milliseconds; TrueTime gives 1 to 7 ms with special hardware. Pick a mechanism whose error is below the gap you need to resolve.
  • Pick the weakest tool that works: a per-entity counter if one writer owns the entity, version vectors if you must detect concurrency, an HLC if you need time-like stamps, a consensus log or TrueTime only if you need a global order.
  • For IDs state the layout and the failure mode: bits for time, worker and sequence, how worker ids are assigned, and what happens when the clock goes back.
  • The pitfall to avoid: using timestamps for ordering and also for expiry without noticing they need different clocks, wall for the first and monotonic for the second.
Before you move on
  • Explain when to use the monotonic clock and when the real-time clock, and what happened to Cloudflare's DNS when it mixed them up.
  • Work out how far a 200 µs per second clock can drift in an hour, and why Spanner needs a poll interval of seconds, not minutes.
  • Run a Lamport clock and a vector clock on a small message diagram, and show an example where Lamport order is misleading and the vectors are not.
  • Apply the HLC rules to a receive event on a node whose clock is behind, and explain what the counter c does.
  • Describe how TrueTime's commit wait gives external consistency, and compute the lifetime and per-millisecond capacity of a 41-bit, 12-bit-sequence Snowflake ID.
Sources and further reading
  • Lamport: Time, Clocks, and the Ordering of Events in a Distributed System (1978) — happened-before, the Clock Condition, logical clocks and the total order
  • Kulkarni et al.: Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases — the hybrid logical clock algorithm
  • Spanner paper (OSDI 2012) — TrueTime, the 200 µs/s drift bound, the 1 to 7 ms uncertainty, commit wait
  • CockroachDB: transaction layer — hybrid-logical clocks, maximum clock offset, read uncertainty
  • RFC 5905: Network Time Protocol version 4 — precision figures, strata and the leap indicator
  • Google Public NTP: leap smear — the 24-hour linear smear and its half-second bound
  • Cloudflare: how and why the leap second affected DNS — a negative duration from a stepped wall clock
  • Linux clock_gettime(3) — CLOCK_REALTIME against CLOCK_MONOTONIC
  • RFC 9562: Universally Unique IDentifiers — UUIDv4 and UUIDv7 layouts, monotonicity, index locality, security notes
  • ULID specification — 48-bit time, 80-bit randomness, 26-character encoding, monotonic sort
  • Twitter Snowflake (archived) — 41-bit time, 10-bit machine, 12-bit sequence, refusing to issue IDs when the clock goes back
  • Flickr: ticket servers — auto-increment tables on two MySQL servers, odd and even

Claims in this chapter were checked against these sources, and against the Dynamo paper, PostgreSQL's sequence and UUID documentation and the Amazon Time Sync Service pages, in October 2026. Drift arithmetic uses the Spanner paper's 200 µs per second bound; the 1012 UUID example is illustrative.

✓Practise

5 questions drawn from 8, scored out of 5. This is practice: it runs in your browser, saves nothing and does not change what you have marked read.

A5

Finished reading?

0 of 36 chapters read in this series

→Next stop

  1. A4Previous · A4Sharding & partitioning at scale
  2. A5you are here
  3. Next · A6Distributed consensus (surface)Getting a group of unreliable machines to agree on one value — and why your job is to use a consensus system correctly, not to write one.19 min read

01On this page

9 sections · 0%

  1. 01Two questions that sound like one
  2. 02Two clocks in every computer, for different jobs
  3. 03How wrong can a clock be?
  4. 04Logical time: order without clocks
  5. 05Hybrid logical clocks: timestamps that look like time and respect causes
  6. 06TrueTime: a clock that reports its own error
  7. 07Generating IDs: uniqueness, order and no coordination
  8. 08Choosing
  9. 09Recognizing it in an unseen problem

[] chapters · t top