Skip to content
advanced Phase 6 · Distributed Systems Fundamentals

Logical Clocks and Event Ordering

Order events across machines where no global clock is available.

55m
0 problems
Topic Progress 0%

Why the Timestamps Lie: Drift, Skew, and the Order You Cannot See

An e-commerce support team investigates a customer's complaint: the audit log shows PAYMENT_RECEIVED at 14:02:31.004 and ORDER_CONFIRMED at 14:02:30.998 — the confirmation before the payment, six milliseconds early. Nothing is broken: the events were written by two services on two hosts whose clocks disagreed by more than the gap between the events. Physical clocks drift, so cross-machine timestamps cannot totally order events alone — and any design that sorts by wall-clock time across nodes has quietly assumed a referee that does not exist.

The numbers explain why. Each machine's oscillator runs slightly fast or slow (tens of ppm: a 50 ppm drift is ~4.3 seconds/day uncorrected). NTP discipline corrects drift but delivers skew: a well-behanged datacenter host typically holds ±1–5 ms, clouds advertise ±10–50 ms, and a laptop across the internet can be seconds off — plus step adjustments when NTP decides to jump. Skew of ±5 ms means any two events within a 10 ms window on different hosts have an unreliable order, which on a busy system is a large fraction of all event pairs.

So what remains true? Lamport's framing: order is defined by causality, not by time. Event a happened before event b (written a → b) when one of three things holds: they are in the same process with a first; a is the send of a message that b receives; or there is a chain linking them (a → b → c implies a → c — the transitive closure). Everything not related by → is concurrent — not "almost simultaneous," but unordered by causality: neither could have known about the other when it ran. The key property to internalize: clocks (physical or logical) can respect causality, but causality is not the same as a total order, and any total order beyond it is an invention.

Why this matters beyond audit logs — three systems that live or die on it:

System What goes wrong without causal order
Replicated key-value store A read-your-writes session sees an older value overwrite a newer one during replication
Event-sourced log "Undo" events replay in an order that corrupts the reconstructed state
Distributed cache invalidation Invalidation arrives before the write it refers to, resurrecting stale data

The practical takeaway for design: use wall-clock timestamps for what they are good for — human-readable display, TTLs, rough bucketing, and single-writer logs where one machine stamps every row (within one writer, its own monotonic clock is internally consistent). For anything cross-node — replication order, deduplication, conflict detection — carry logical ordering information in the payload, because you cannot fix skew by buying better clocks. Google's TrueTime (±7 ms with a commit-wait protocol) shows what it costs to make physical clocks trustworthy enough for external consistency: a global database built around the uncertainty, not a setting you flip in NTP. For everyone else, the correct instinct when an ordering bug appears is to stop debugging timestamps and start asking: what does causality say, and is this pair actually concurrent? Answering that question needs the counters of the next two chapters — starting with the simplest counter that has ever been made correct.

Lamport's Counter: The Smallest Mechanism That Respects Causality

Leslie Lamport's 1978 result fits on an index card: maintain one integer per process, follow three rules, and you obtain a clock whose ticks never contradict causality.

L1  before delivering any event (including each local event), increment the counter
L2  when sending a message, attach the current counter in the header
L3  on receive, set counter = max(local, received) and then increment

That's the whole protocol — no coordination, no NTP, no network traffic beyond a field on messages you were sending anyway. The theorem it buys: if a → b then L(a) < L(b) — causally ordered events get strictly increasing timestamps. A message's receive is forced above its send, and transitivity propagates that through every chain, so a causal ancestor can never be misfiled as later than its descendant.

Work a tiny trace, because the trace exposes the subtlety:

N1: L1=5  send m1 carrying 5        N2: L2=3  send m2 carrying 3
N1: recv m2 (3) → max(5,3)=5, inc → L1=6        N2: recv m1 (5) → max(3,5)=5, inc → L2=6

Both processes now carry 6 — and their sends m1 and m2 were concurrent: neither carried knowledge of the other. Their Lamport values happened to collide here (both 5 at send time), which is legal and frequent. Two rules follow. First, the converse is false and this is the trap: L(a) < L(b) does not prove a → b; it merely fails to contradict causality. Second, ties are broken mechanically — order by (L, nodeId) — giving a deterministic total order everyone computes identically. Deterministic, but arbitrary: the tie-break says nothing about what actually happened, and mistaking that arbitrariness for truth is how "deterministic total order" gets sold as "the real order of events."

What Lamport clocks cannot do is the thing engineers often need most: detect concurrency. If your collaboration feature must ask "did these two edits happen without seeing each other — should I merge or discard one?", a pair of scalars cannot answer; both values rising says nothing about whether one edit knew of the other. The analogy: a scalar counter preserves differences (causally later is bigger) but destroys shape (was there a path between them?).

Where scalar clocks legitimately suffice in production: single-writer logs and sequences (the writer assigns its own increasing values — a database sequence, a Kafka offset within a partition); fencing (epochs only need to increase, never to describe causality); tie-breakers inside an already-causal protocol; and freshness heuristics like "prefer the replica with the higher acked index." All four use only the guaranteed direction of the theorem and none need the converse.

Interview framing: expect two questions. State the three rules — increment on event, send your value, receive with max-then-increment (the max before increment is what makes cross-chains converge upward). And what can't it do? — order concurrent events meaningfully; a smaller Lamport value means "not causally after," never "happened first." That second answer is the bridge to vector clocks, which keep exactly the information the scalar throws away — at the cost of a value per participant.

Vector Clocks: Detecting the Concurrent Updates You Must Merge

A contact's phone number is edited on two devices while offline. Device A sets 555-0100; device B sets 555-0199. They reconnect and replication delivers the writes to a shared store. A scalar clock (or a Lamport value with node-id tie-break) silently keeps one and drops the other — the user who typed the winning number watches their edit vanish with no warning. Vector clocks track causal relationships so concurrent updates can be detected, which turns silent loss into an explicit merge.

The mechanism replaces one integer with a small array — one counter per participant:

before local event:      V[self] += 1
send message m:          attach a copy of V; then V[self] += 1  (equivalently inc then send)
on receive of m with Vm: for each i: V[i] = max(V[i], Vm[i]); V[self] += 1

Now each event carries a history summary — "what I knew when I happened" — and comparison of two vectors X and Y yields exactly the three-way answer causality allows:

X == Y        same knowledge — identical logical state
X < Y         happened-before: Y knows everything X does, and more  (X[i] ≤ Y[i] ∀i, one strict)
X || Y        CONCURRENT: neither learned of the other — a merge decision is required

The concurrent verdict is the entire product. On receiving two versions of phone, the store does not guess which is "later"; it sees V_A || V_B, and because the conflict is now visible, it can apply a strategy chosen for the data type:

Data type Resolution that preserves correctness
Growable set (shopping cart, tags) Merge by union/CRDT semantics — both edits survive
Field-level last-writer (profile) Keep per-field versions; show both to the user if the values differ
Registers with no merge (a single scalar) App-supplied merge function, or surface the conflict
Anything with invariants (bank balance) Concurrency must never be silently merged — route to a resolver, or serialize the operation in the first place

Production systems stand on this design. Dynamo and DynamoDB's DynamoDB-style items record vector versions and offer explicit conflict handling; Riak stored sibling values until an application resolved them; every offline-first sync engine (the phone-contact case) is vector clocks under a friendlier name. Note the costs, because they bound where the pattern applies: the vector grows with the number of participants (bounded in practice by pruning stale entries or tracking only active writers), comparisons are cheap but storage is not free, and — crucially — the vector detects that a merge is needed; it does not perform the merge. The resolution policy (union? keep both? ask the user?) is a domain decision that no clock can supply.

A common confusion worth dispelling for interviews: vector clocks order events, and "last-writer-wins" by wall-clock timestamp is a deliberate discarding of that information — it is cheap, and it is exactly how the phone number got lost. The senior-level answer names the spectrum: timestamp LWW for ignorable fields, vector-detected merges for user data, and serialization (transactions, single-writer) wherever invariants forbid concurrent resolution at all. Then the natural closing line: having agreed on order, we are finally ready to ask the harder question — how do replicas agree on a single value when one of them may be wrong? That is consensus, and Raft is the mechanism you meet next.

Quiz

1. Two services log events 6 ms apart on different hosts, with NTP skew of ±5 ms. The log shows the later event first. What is the correct conclusion?

Question 1 options

2. Lamport's rules: a process receives message m with counter 7 while its own counter is 12. What does it do, and what does the result prove?

Question 2 options

3. Device A edits a contact to X with vector [2,1]; device B independently edits it to Y with vector [1,2]. What does comparison report and what should happen?

Question 3 options

4. Which workload can safely rely on Lamport-style scalar counters alone?

Question 4 options

Flashcards

Question

Why can't physical timestamps totally order events across machines?

Answer

Clocks drift (tens of ppm) and NTP leaves real skew: ±1–5 ms well-managed datacenter, ±10–50 ms cloud, seconds for laptops — so any two events within the skew window have unreliable order. Timestamps remain valid for display, TTLs, and single-writer logs; cross-node order needs causal information (message-carried counters), as TrueTime's commit-wait protocol shows for the physical-clock route.

Question

Lamport's three rules and the exact guarantee they provide.

Answer

L1 increment the counter before each local event/delivery. L2 attach the counter when sending. L3 on receive: counter = max(local, received), then increment. Guarantee: a → b implies L(a) < L(b) — the clock never contradicts causality. Non-guarantee: L(a) < L(b) does not prove a → b; concurrent events can share values (tie-break with node id for a deterministic but arbitrary total order).

Question

How does a vector clock compare two versions — the three outcomes?

Answer

Equal (X == Y): same logical state. Happens-before (X[i] ≤ Y[i] for all i, strictly less in one): Y subsumes X, keep Y. Concurrent (neither dominates): neither edit learned of the other — a merge decision is required. The third outcome is the product: silent 'latest timestamp wins' is precisely how concurrent updates get lost.

Question

What do vector clocks NOT do, and who must decide the merge?

Answer

They detect that versions are concurrent; they do not resolve them. The resolution is a domain decision: union for growable sets (CRDT semantics), per-field versions for profiles, explicit conflict surfaced to the user, and — where invariants like a bank balance forbid concurrent resolution — serialization (transaction/single-writer) instead of merging.

Question

When is scalar (Lamport) ordering sufficient in production?

Answer

When only 'causally later ⇒ larger' is consumed: single-writer sequences (Kafka offsets, DB sequences), fencing epochs (must increase, not describe causality), tie-breakers inside an already-causal protocol, and replica-freshness heuristics. It fails the moment you must detect that two updates are concurrent — offline sync, multi-master, cart merges need vectors.

Revision Notes

Key Takeaways

  • 1. Causality, not wall-clock time, defines order — skew makes cross-host timestamp sorting unreliable, and perfect clocks (TrueTime) require a protocol around their uncertainty
  • 2. Lamport's max-then-increment rules guarantee cause < effect but not the converse: a smaller value means 'not causally after,' never 'happened first'
  • 3. Concurrent means neither event knew of the other — scalar counters destroy that shape, which is why they cannot detect the conflicts users actually lose data to
  • 4. Vector clocks' three-way verdict (equal, subsumed, concurrent) converts silent lost updates into visible merge decisions
  • 5. Detection is not resolution: the merge policy belongs to the data type — union for sets, versions for fields, serialization where invariants forbid merging

Interview Tips

  • • 'Why can't you order events by timestamp?' — cite skew numbers (±5 ms NTP, ±10–50 ms cloud) versus sub-millisecond event gaps; then say what timestamps are still fine for (display, TTL, single-writer)
  • • 'State Lamport's rules' — increment on event, carry on send, max-then-increment on receive; then immediately add the one-way guarantee and the (L, nodeId) tie-break being arbitrary
  • • 'What is a vector clock and when would you use one?' — per-writer counters, three-way comparison, concurrent ⇒ merge; anchor with the offline phone-number edit example
  • • 'How do you resolve the conflict once detected?' — by data type: CRDT union for sets, field versions for profiles, explicit serialization for anything with invariants; last-writer-wins is discarding information, say so knowingly

Cheat Sheet

Logical Clocks & Event Ordering

Why timestamps fail cross-node: drift tens of ppm; NTP skew ±1–5 ms (DC) / ±10–50 ms (cloud) / seconds (edge) — any pair closer than the skew has unreliable order. Time for display, TTLs, single-writer logs; causality for order.

Happened-before (→): same process order · send→receive · transitive closure. Not related by → = concurrent (unordered by causality, not 'nearly simultaneous').

Lamport (scalar):

L1 local event:      L += 1
L2 send m:           carry L
L3 receive m:        L = max(L, received); L += 1
guarantee:  a → b  ⇒  L(a) < L(b)        NOT the converse

Tie-break (L, nodeId) → deterministic total order — deterministic ≠ real.

Vector:

local event:  V[self] += 1        receive: V[i] = max(V[i], Vm[i]); V[self] += 1
compare:  X == Y same · X < Y subsumed · X || Y CONCURRENT → merge decision

Merge policy by type: growable set → union/CRDT · profile field → per-field versions · scalar register → app merge fn or surface conflict · invariant data (balance) → serialize, never merge silently.

Spectrum: timestamp LWW (ignorable fields, loses data) · scalar (single writer, fencing) · vector-detected merge (user data, offline sync) · serialization (invariants).

Next question: order agreed → how do replicas agree on one value? → consensus (Raft).

Continue Learning