Skip to main content

Logical Time: Lamport & Vector Clocks

Level 5: Level 5: Distributed Systems Corehard30 minlogical-clocksvector-clockscausality

Lamport clocks give a causality-respecting total order but cannot detect concurrency; vector clocks detect it and surface siblings, at O(N) size with a GC problem.

Ordering events without a shared clock

In a distributed system you cannot trust wall clocks to order events, and there is no shared clock at all. Yet you constantly need to answer "did A happen before B, or were they concurrent?" Logical clocks answer that using only message passing, via Lamport's happens-before relation (written A → B):

  • If A and B are on the same node and A came first, then A → B.
  • If A is a send and B is the matching receive, then A → B.
  • Transitivity: if A → B and B → C then A → C.
  • If neither A → B nor B → A, the events are concurrent, written A ∥ B. Concurrency is the interesting case: it is where two clients may have independently updated the same thing.

Lamport clocks

Each node keeps an integer counter. Increment it on every local event; stamp outgoing messages; on receive, set your counter to max(local, received) + 1. The guarantee: if A → B then L(A) < L(B). This gives a total order (break ties by node id) that never contradicts causality: enough to agree on a single order for a replicated log.

Check yourself
Event A has Lamport timestamp 4 and event B has timestamp 9, on different nodes. What do the stamps prove?

The catch, and the single most-probed point here: the implication only goes one way. L(A) < L(B) does not imply A → B. Two concurrent events on different nodes can have any Lamport values, so a smaller timestamp tells you nothing about causation. Lamport clocks cannot detect concurrency. They can order everything; they cannot tell you which orderings were forced by causality and which were arbitrary.

Vector clocks

Each node keeps a vector with one counter per node. On a local event, increment your own slot. On send, attach your whole vector. On receive, take the element-wise max, then increment your own slot. Compare two vectors:

  • V(A) < V(B) (every element ≤, at least one <) means A → B.
  • V(B) < V(A) means B → A.
  • Neither dominates means A ∥ B, concurrent, and if they touched the same key, a conflict.
Check yourself
V(A) is [2,1,0] and V(B) is [1,2,0]. What is the relationship?
node A: [1,0,0] --send--> node B receives, takes max, bumps self -> [1,1,0]
meanwhile node C, no contact: [0,0,1]
compare [1,1,0] vs [0,0,1]: A-slot 1>0 but C-slot 0<1 -> neither dominates -> CONCURRENT

That detection is why Dynamo and Riak use vector clocks (technically version vectors, one entry per replica) to surface siblings: when a read finds two concurrent versions, it returns both to the application (or a merge function / LWW / CRDT) rather than silently picking one and losing a write.

Interview nuance, the costs. Vector clocks are O(N) in the number of participants. Worse, in a system with many transient actors (mobile clients writing directly), the vector grows without bound because each new writer adds a slot, and you cannot easily garbage-collect entries for actors that may still return. This GC / unbounded-growth problem is why Dynamo keys version vectors on the small fixed set of storage nodes rather than per-client, and why pruning old entries risks false-concurrent readings.

Recap: Lamport clocks give a causality-respecting total order but cannot detect concurrency; vector/version clocks do detect concurrency and let leaderless stores surface conflict siblings, at O(N) size and a real garbage-collection problem when actors churn.

Check yourself

Last pass before the design write: which clock does each job describe?

Agree on one total order of entries for a replicated log
Detect that two writes to a key were concurrent and surface both as siblings
Stay constant-size no matter how many writers show up
Risks unbounded growth, and false concurrency if old entries are pruned

Apply

Your turn

The task this lesson builds to.

Use vector clocks to detect concurrent conflicting writes in a leaderless key-value store, and specify how the read path surfaces siblings.

Think about

  1. Why can Lamport clocks give a total order but not detect concurrency?
  2. What do vector clocks capture that Lamport clocks cannot?
  3. What is the O(N) cost and GC problem of vector clocks?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design conflict detection and resolution for a collaborative note-taking app like Notion or Apple Notes syncing across a laptop, phone, and tablet that all edit the same note offline and reconnect, at a scale of millions of devices. Explain why plain vector clocks keyed on devices are a trap here and what you use instead.

Think about

  1. What happens to a device-keyed vector when the device population is huge and churning?
  2. What converts 'concurrent edit' into automatic convergence instead of a sibling to reconcile?
  3. Where can a small version vector still be used safely?