Distributed & Decentralized Systems Curriculum
Time Causality · Vector Clocks

Key Question

How can we extend Lamport’s idea to capture causality accurately across multiple processes?

Deep Dive

Lamport clocks use a single integer counter. This is insufficient for detecting concurrency because a single integer cannot distinguish between “I processed fewer events” and “your event is causally after mine.” Vector clocks solve this by using an array of N integers, where N is the number of processes in the system. Each element of the vector tracks the Lamport clock of the corresponding process.

The rules for vector clocks are a direct extension of Lamport’s rules:

  1. Initialize: Each process maintains vector V = [0, 0, …, 0] of length N.
  2. Local event: V[i] += 1 (increment the element corresponding to this process).
  3. Send: V[i] += 1, then include the entire vector V in the message.
  4. Receive: For each element j: V[j] = max(V[j], Vm[j]) where Vm is the vector from the message. Then V[i] += 1.

The key difference from Lamport clocks is that the receiver updates ALL elements of its vector to the maximum of its own and the sender’s, not just a single counter. This means each process carries a summary of the last known state of every other process.

Let us walk through a 3-process example step by step:

Process A (idx 0)   Process B (idx 1)   Process C (idx 2)
    V_A = [0,0,0]       V_B = [0,0,0]       V_C = [0,0,0]

Step 1: Process A has a local event.

V_A = [1,0,0]    (V_A[0] incremented)

A’s vector says: “I know my own state is version 1, and I know nothing about B or C.”

Step 2: Process A sends a message to B.

A increments: V_A = [2,0,0]
Message carries: [2,0,0]

Step 3: Process B receives the message from A.

B's current vector: V_B = [0,0,0]
B applies: V_B = max([0,0,0], [2,0,0]) = [2,0,0]
B increments its own: V_B = [2,1,0]

B’s vector says: “I know A is at version 2, I am at version 1, and I know nothing about C.” Crucially, B now knows that it has seen everything A had seen up to A’s version 2.

Step 4: Process B sends a message to C.

B increments: V_B = [2,2,0]
Message carries: [2,2,0]

Step 5: Process C receives the message from B.

C's current vector: V_C = [0,0,0]
C applies: V_C = max([0,0,0], [2,2,0]) = [2,2,0]
C increments its own: V_C = [2,2,1]

C knows: “A version 2, B version 2, C version 1.”

Space-time diagram showing vector evolution:

Process A          Process B          Process C
[0,0,0]            [0,0,0]            [0,0,0]
  |                   |                  |
a1 [1,0,0]            |                  |
  |                   |                  |
a2 [2,0,0] ─────────→ b1 [2,1,0]        |
  |                   |                  |
a3 [3,0,0]            |                  |
  |                   b2 [2,2,0] ───────→ c1 [2,2,1]
  |                   |                  |
  |                   b3 [2,3,0]         c2 [2,2,2]
  |                   |                  |

Now compare events using their vectors:

  • a1 = [1,0,0], b3 = [2,3,0] Is a1 → b3? [1,0,0] < [2,3,0] (every element of a1 ≤ corresponding element of b3, and at least one is strictly less). Yes, a1 → b3.

  • b2 = [2,2,0], c2 = [2,2,2] Is b2 → c2? [2,2,0] < [2,2,2] (C’s element is greater). Yes, b2 → c2.

  • a3 = [3,0,0], c1 = [2,2,1] [3,0,0] vs [2,2,1]: 3 > 2 (A’s element), 0 < 2 (B’s element), 0 < 1 (C’s element). Neither dominates. a3 || c1 (concurrent).

We have successfully detected that a3 and c1 are concurrent — something Lamport clocks could not do.

Check Your Understanding

  1. In the 3-process example, what is the vector clock value at b3?
  2. What is the purpose of the max() operation when receiving a message, applied to each element of the vector?
  3. Why does using N integers (one per process) provide more information than a single integer counter?

The “So What?”

Vector clocks are the standard tool for causality tracking in distributed systems. They appear in Amazon Dynamo (as version vectors), in Bayou’s database replication, in CRDTs for collaborative editing, and in distributed debuggers for causal tracing. The added cost — storing N integers per event and per message — is the price of accurate conflict detection. When you see a system that can tell you whether two updates are in conflict, vector clocks (or their close relatives) are almost certainly at work.


✏️ Exercises

Topic 5: Vector Clocks — Exercises

Exercise 1: Comparing Vectors

Given the following pairs of vector clocks from a 3-process system (processes A, B, C), determine whether the events are causally related (V1 < V2 or V2 < V1) or concurrent (V1 || V2).

PairV1V2
(a)[2, 1, 4][1, 2, 3]
(b)[3, 0, 2][3, 1, 2]
(c)[1, 2, 3][1, 2, 3]
(d)[2, 2, 2][3, 3, 3]
(e)[2, 0, 0][1, 1, 1]

For each pair, state whether V1 → V2, V2 → V1, or V1 || V2, and explain your reasoning.


Exercise 2: Causal Ordering

In a 3-process system, the following events occur:

  1. Process A: local event X → V_A = [1, 0, 0]
  2. Process B: local event Y → V_B = [0, 1, 0]
  3. Process A: sends message to C → V_A = [2, 0, 0], message carries [2, 0, 0]
  4. Process C: receives message from A → V_C = [2, 0, 1]
  5. Process C: sends message to B → V_C = [2, 0, 2], message carries [2, 0, 2]
  6. Process B: receives message from C → V_B = [2, 1, 2] (after max, then increment)

(a) Is event X causally related to event Y? What does the vector clock comparison show? (b) After step 6, what is the complete vector clock state on all three processes? (c) If a new event occurs on Process C after step 5 (without any new messages), what would its vector clock be?


Exercise 3: Dynamo Conflict Scenario

A 2-node Dynamo-style key-value store stores a document with key “profile” = { “name”: “Alice” }. The initial version vector is [(A, 1)].

The following events occur concurrently:

  • Client 1 reads “profile” from Node A. Gets vector [(A, 1)], value = { “name”: “Alice” }. Client 1 adds field “age”: 30 and writes back to Node A.
  • Client 2 reads “profile” from Node B. Gets vector [(A, 1)], value = { “name”: “Alice” }. Client 2 adds field “email”: “alice@example.com” and writes back to Node B.

(a) What is the version vector of Client 1’s write? Of Client 2’s write? (b) A third client reads “profile” from Node A. Node A has received both writes through replication. What vectors does the client receive? Are they concurrent? (c) The application uses a merge function that combines fields: the result should include all fields from both versions. What is the merged value and the merged version vector? (d) After the merge is written back, a fourth client reads the key. What vector does it receive, and what does it tell the reader about this version relative to the two original writes?


Exercise 4: LWW vs. Sibling Preservation

A social media application stores user posts in a Dynamo-style database. Each post has a “like count” and a “comments list.”

(a) Which field would benefit from sibling preservation (returning multiple versions for merge)? Which could safely use LWW? Explain your reasoning. (b) If the like count uses LWW and the comments list uses sibling preservation, how does the application handle a read that returns both a single value (for like count) and multiple siblings (for comments)? (c) Design a scenario where LWW on the like count loses exactly one “like” even though the user pressed the like button. (d) Would a vector clock implementation detect this lost update? Why or why not?

👁️ View Solutions

Topic 5: Vector Clocks — Solutions

Solution 1

(a) [2, 1, 4] vs [1, 2, 3]

  • Position 0: 2 > 1 (V1 wins)
  • Position 1: 1 < 2 (V2 wins)
  • Position 2: 4 > 3 (V1 wins)

V1 has elements both greater and less than V2. Neither dominates. V1 || V2 (concurrent).

(b) [3, 0, 2] vs [3, 1, 2]

  • Position 0: 3 ≤ 3 ✓
  • Position 1: 0 ≤ 1 ✓
  • Position 2: 2 ≤ 2 ✓

V1 ≤ V2, and strictly less at position 1. V1 → V2 (V1 happened before V2). Process B processed one more event (or learned about one more) between the two events.

(c) [1, 2, 3] vs [1, 2, 3]

  • All elements equal:

V1 ≤ V2 (all equal) AND V2 ≤ V1 (all equal). The vectors are identical. These events are causally equivalent — they have the same causal history. They are not concurrent in the sense of being incomparable. This represents two events that have seen exactly the same state from all processes.

(d) [2, 2, 2] vs [3, 3, 3]

  • Position 0: 2 ≤ 3 ✓
  • Position 1: 2 ≤ 3 ✓
  • Position 2: 2 ≤ 3 ✓

V1 ≤ V2, strictly less in all positions. V1 → V2. Event V1 definitely happened before V2.

(e) [2, 0, 0] vs [1, 1, 1]

  • Position 0: 2 > 1 (V1 wins)
  • Position 1: 0 < 1 (V2 wins)
  • Position 2: 0 < 1 (V2 wins)

V1 has element greater (position 0) and elements less (positions 1, 2). Neither dominates. V1 || V2 (concurrent). Process A had more local events, but B and C had events that A did not know about, and vice versa.


Solution 2

(a) X has V = [1, 0, 0] and Y has V = [0, 1, 0].

Compare: [1, 0, 0] vs [0, 1, 0]:

  • Position 0: 1 > 0 (X wins)
  • Position 1: 0 < 1 (Y wins)
  • Position 2: 0 ≤ 0

Neither dominates. X || Y (concurrent). X and Y are independent local events on different processes with no causal connection.

(b) After step 6:

  • Process A: [2, 0, 0] (unchanged since step 3)
  • Process B: [2, 1, 2] (computed in step 6)
  • Process C: [2, 0, 2] (unchanged since step 5)

(c) After step 5, Process C’s vector is [2, 0, 2]. A new local event on C would increment C’s own element: V_C = [2, 0, 3].


Solution 3

(a)

Client 1’s write at Node A: increments A’s counter. Vector = [(A, 2)].

Client 2’s write at Node B: increments B’s counter. Vector = [(A, 1), (B, 1)].

(b) The third client receives both vectors:

  • Version X: vector [(A, 2)], value = { “name”: “Alice”, “age”: 30 }
  • Version Y: vector [(A, 1), (B, 1)], value = { “name”: “Alice”, “email”: “alice@example.com” }

Compare [(A, 2)] vs [(A, 1), (B, 1)]:

  • A: 2 > 1 (X wins on A)
  • B: X has no B entry (implicitly 0), Y has B=1. 0 < 1 (Y wins on B)

Neither dominates. The versions are concurrent. The client receives siblings.

(c) The merge function combines fields:

Merged value: { “name”: “Alice”, “age”: 30, “email”: “alice@example.com” }

Merged vector: [(A, 2), (B, 1)] — the element-wise maximum of both vectors.

Check that it dominates both parent vectors:

  • X = [(A, 2)]: A: 2 ≥ 2 ✓ (B implicit 0, merged B=1 ≥ 0 ✓) → merged ≥ X
  • Y = [(A, 1), (B, 1)]: A: 2 ≥ 1 ✓, B: 1 ≥ 1 ✓ → merged ≥ Y

Both prior versions are in the causal past of the merged version.

(d) The fourth client reads and gets a single version: vector [(A, 2), (B, 1)], value = { “name”: “Alice”, “age”: 30, “email”: “alice@example.com” }.

The vector tells the reader: “This version incorporates everything from Node A up to counter 2 and Node B up to counter 1.” Since the original two writes had vectors [(A, 2)] and [(A, 1), (B, 1)], and both are ≤ this merged vector, the reader knows they have seen all prior versions. No siblings — the conflict was resolved.


Solution 4

(a) Comments list benefits from sibling preservation. If two users concurrently add comments to a post, both comments should appear in the final list. LWW would lose one user’s comment.

Like count could safely use LWW. If two users concurrently like a post, the like count should ideally increase by 2. But with LWW, one like would be lost. Whether this is acceptable depends on whether exact like counts matter. For many applications, an approximate count is acceptable; for others (contests, trending algorithms), it matters.

Better approach: treat the like count as an operation-based CRDT (each like increments a counter, and increments commute) rather than LWW.

(b) The application receives:

  • For like count (LWW): a single value — say, 42.
  • For comments (siblings): two versions — [“Great post!”] and [“Nice work!”].

The application merges the comments: [“Great post!”, “Nice work!”] and takes the like count as-is (42). When writing back, the application writes both fields together with a new version vector that dominates the parent vectors.

(c) Scenario:

  • User A sees like count = 10, vector = [(A, 5)]
  • User A presses like. The write: like count = 11, vector = [(A, 6)]
  • Concurrently, User B sees like count = 10, vector = [(A, 5)] (hadn’t received A’s write yet)
  • User B presses like. The write: like count = 11, vector = [(A, 5), (B, 1)]

If the system uses LWW and compares by timestamp:

  • User A’s write timestamp: 10:00:01 (Node A clock)
  • User B’s write timestamp: 10:00:00.500 (Node B clock, which is slightly behind)

LWW picks User A’s write: like count = 11. But there should be 12 (two likes from two users). One like is lost.

(d) Yes, a vector clock implementation would detect this conflict:

  • User A’s write: [(A, 6)] vs User B’s write: [(A, 5), (B, 1)]
  • A: 6 > 5 (A wins), B: 0 < 1 (B wins).
  • Neither dominates: concurrent writes detected!

The system would present both versions to the application. The application would compute: the correct like count is the maximum of both (11) plus the additional likes each version represents. Version A has 1 new like (11 - 10 = 1). Version B has 1 new like (11 - 10 = 1). Total: 10 + 1 + 1 = 12. Vector clocks saved the lost like.

🎮 Interactive Vector Clock
Scroll to load interactive visualization…