Loading learning content...
Last-Write-Wins tells us which value to keep, but not why the values diverged or whether they could have influenced each other. Vector clocks solve a different problem: they tell us whether two operations are causally related or genuinely concurrent.
This distinction matters enormously. If operation B saw the result of operation A before proceeding, then B is not in conflict with A—it supersedes it. But if A and B happened independently, neither knew about the other, they are genuinely concurrent, and we have a real conflict that requires resolution.
Vector clocks give us the tools to make this determination, enabling smarter conflict handling than simple timestamp comparison.
By the end of this page, you will understand the mathematical foundation of vector clocks, how to implement and compare them, how they detect true concurrency versus causal ordering, their practical applications in distributed databases, and their limitations in real-world systems.
Before diving into vector clocks, let's understand why scalar (single-value) timestamps are insufficient for distributed conflict detection.
Lamport timestamps (single counters) guarantee that if A happened before B, then timestamp(A) < timestamp(B). But the converse is not true: if timestamp(A) < timestamp(B), we cannot conclude that A happened before B. The events might be concurrent.
The Gap in Lamport Clocks:
| Relationship | Lamport Guarantee |
|---|---|
| A → B (A happened before B) | ts(A) < ts(B) ✓ |
| ts(A) < ts(B) | Maybe A → B, maybe concurrent (?) |
| ts(A) = ts(B) | Concurrent or same event (?) |
We need a clock that can definitively answer: Were these events concurrent, or did one causally precede the other?
Vector clocks provide exactly this capability. By tracking the knowledge each node has about other nodes' progress, we can determine causal relationships—or their absence.
Event A 'happens before' B (written A → B) if: (1) A and B are on the same node and A occurred first, OR (2) A is a send and B is the corresponding receive, OR (3) there exists an event C where A → C and C → B (transitivity). Events are 'concurrent' (A || B) if neither A → B nor B → A.
A vector clock is an array of counters, one for each node (or actor) in the distributed system. Each node maintains its own vector clock and updates it according to specific rules.
Structure:
For a system with N nodes, a vector clock is: VC = [c₁, c₂, c₃, ..., cₙ]
Where cᵢ represents the number of events that have occurred at node i, as known by the node holding this vector clock.
Rules:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
interface VectorClock { [nodeId: string]: number;} /** * Vector Clock implementation for distributed causality tracking. */class VectorClockManager { private clock: VectorClock; private nodeId: string; constructor(nodeId: string, knownNodes: string[] = []) { this.nodeId = nodeId; this.clock = {}; // Initialize all known nodes to 0 for (const node of knownNodes) { this.clock[node] = 0; } this.clock[nodeId] = 0; } /** * Increment clock for a local event. */ tick(): VectorClock { this.clock[this.nodeId] = (this.clock[this.nodeId] ?? 0) + 1; return this.getClock(); } /** * Prepare clock to send with a message (tick first). */ send(): VectorClock { return this.tick(); // Local event: sending } /** * Update clock on receiving a message. */ receive(remoteClock: VectorClock): VectorClock { // Merge: take component-wise maximum const allNodes = new Set([ ...Object.keys(this.clock), ...Object.keys(remoteClock), ]); for (const node of allNodes) { const local = this.clock[node] ?? 0; const remote = remoteClock[node] ?? 0; this.clock[node] = Math.max(local, remote); } // Then increment our own counter (receiving is a local event too) this.clock[this.nodeId] = (this.clock[this.nodeId] ?? 0) + 1; return this.getClock(); } getClock(): VectorClock { return { ...this.clock }; }} // Example: Three-node systemconst nodeA = new VectorClockManager('A', ['A', 'B', 'C']);const nodeB = new VectorClockManager('B', ['A', 'B', 'C']);const nodeC = new VectorClockManager('C', ['A', 'B', 'C']); // Node A performs a local eventconst vc1 = nodeA.tick();console.log('After A event:', vc1); // { A: 1, B: 0, C: 0 } // Node A sends to Bconst msgToB = nodeA.send();console.log('A sends:', msgToB); // { A: 2, B: 0, C: 0 } // Node B receives from Aconst vc2 = nodeB.receive(msgToB);console.log('B after receive:', vc2); // { A: 2, B: 1, C: 0 }Intuition:
Each node's vector clock represents its knowledge horizon—what it knows about the progress of all nodes. When Node B receives a message from Node A containing A's vector clock, B learns about all events that A knew about when it sent the message. By taking the maximum, B incorporates A's knowledge into its own.
The power of vector clocks lies in their comparison semantics. Given two vector clocks VC₁ and VC₂, we can determine their causal relationship.
| Condition | Relationship | Interpretation |
|---|---|---|
| VC₁[i] ≤ VC₂[i] for all i, and VC₁ ≠ VC₂ | VC₁ < VC₂ (VC₁ happened before VC₂) | Event 1 causally precedes Event 2 |
| VC₁[i] ≥ VC₂[i] for all i, and VC₁ ≠ VC₂ | VC₁ > VC₂ (VC₂ happened before VC₁) | Event 2 causally precedes Event 1 |
| VC₁[i] = VC₂[i] for all i | VC₁ = VC₂ (equal) | Same event or identical state |
| Neither VC₁ ≤ VC₂ nor VC₁ ≥ VC₂ | VC₁ || VC₂ (concurrent) | Genuinely concurrent events! |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
interface VectorClock { [nodeId: string]: number;} type VectorComparison = | 'BEFORE' // VC1 happened before VC2 (VC1 < VC2) | 'AFTER' // VC1 happened after VC2 (VC1 > VC2) | 'EQUAL' // Identical clocks | 'CONCURRENT'; // Neither dominates - true conflict! function compareVectorClocks( vc1: VectorClock, vc2: VectorClock): VectorComparison { const allNodes = new Set([ ...Object.keys(vc1), ...Object.keys(vc2), ]); let vc1LessOrEqual = true; // All vc1[i] <= vc2[i] let vc2LessOrEqual = true; // All vc2[i] <= vc1[i] let anyDifferent = false; for (const node of allNodes) { const v1 = vc1[node] ?? 0; const v2 = vc2[node] ?? 0; if (v1 > v2) vc1LessOrEqual = false; if (v2 > v1) vc2LessOrEqual = false; if (v1 !== v2) anyDifferent = true; } if (!anyDifferent) { return 'EQUAL'; } if (vc1LessOrEqual && !vc2LessOrEqual) { return 'BEFORE'; // vc1 < vc2 } if (vc2LessOrEqual && !vc1LessOrEqual) { return 'AFTER'; // vc1 > vc2 } // Neither dominates the other return 'CONCURRENT';} // Examples:// Causal relationship: e1 happened before e2const e1: VectorClock = { A: 2, B: 1, C: 0 };const e2: VectorClock = { A: 3, B: 2, C: 1 };console.log(compareVectorClocks(e1, e2)); // 'BEFORE' // Concurrent events: neither happened before the otherconst e3: VectorClock = { A: 2, B: 1, C: 0 };const e4: VectorClock = { A: 1, B: 2, C: 0 };console.log(compareVectorClocks(e3, e4)); // 'CONCURRENT' - True conflict! // e3 has A:2 > A:1, so e3 is not <= e4// e4 has B:2 > B:1, so e4 is not <= e3// Neither dominates → CONCURRENTWhen two operations are concurrent, LWW would pick one and discard the other. But vector clocks reveal that both operations are 'equally valid'—neither knew about the other. This information enables smarter resolution: perhaps both values should be kept as siblings, or merged using application logic, rather than arbitrarily discarding one.
Let's trace through a detailed example of vector clocks detecting concurrent writes in a three-node system.
Step-by-Step Analysis:
| Time | Event | A's Clock | B's Clock | C's Clock |
|---|---|---|---|---|
| T1 | A writes x=1 | [1,0,0] | [0,0,0] | [0,0,0] |
| T2 | A syncs to B | [1,0,0] | - | [0,0,0] |
| T3 | B receives | [1,0,0] | [1,1,0] | [0,0,0] |
| T4 | B writes x=2 | [1,0,0] | [1,2,0] | [0,0,0] |
| T4' | C writes x=3 (concurrent!) | [1,0,0] | [1,2,0] | [0,0,1] |
At T4/T4':
Comparison:
Vector clocks only DETECT concurrency—they don't resolve it. When concurrent versions are discovered, the system must choose: (1) Return both versions to the application for manual resolution, (2) Apply a deterministic merge function, (3) Use LWW as a fallback, or (4) Invoke application-specific logic. The 'right' choice depends on your domain.
Several distributed databases use vector clocks (or variants) for conflict detection. Understanding their implementations illuminates practical considerations.
Riak's Vector Clocks
Riak was one of the most prominent users of vector clocks for conflict detection. Each object stores a vector clock with its value.
How Riak Uses Vector Clocks:
The Sibling Problem:
When conflicts occur, Riak keeps ALL concurrent versions (siblings). This is correct but creates overhead:
Client-Side Resolution Pattern:
123456789101112131415161718192021222324252627282930313233343536373839404142
// Riak-style sibling resolution (pseudocode)interface RiakObject<T> { values: T[]; // All sibling values vclock: VectorClock; // Vector clock for the read} async function readWithResolution<T>( key: string, resolver: (siblings: T[]) => T): Promise<T> { const obj = await riak.get(key); if (obj.values.length === 1) { // No conflict return obj.values[0]; } // Conflict detected! Multiple siblings. console.log(`${obj.values.length} siblings detected for ${key}`); // Application resolves const resolved = resolver(obj.values); // Write resolved value back with parent's vector clock await riak.put(key, resolved, obj.vclock); return resolved;} // Example resolver for shopping cart: union of all itemsfunction cartResolver(carts: Cart[]): Cart { const allItems = new Map<string, CartItem>(); for (const cart of carts) { for (const item of cart.items) { const existing = allItems.get(item.id); if (!existing || item.quantity > existing.quantity) { allItems.set(item.id, item); } } } return { items: Array.from(allItems.values()) };}While theoretically elegant, vector clocks have practical challenges in production systems.
Vector Clock Size: A Concrete Example
| System Configuration | Vector Size per Object |
|---|---|
| 5-node cluster | 5 × 8 bytes = 40 bytes |
| 100-node cluster | 100 × 8 bytes = 800 bytes |
| Per-client (10K clients) | 10,000 × 8 bytes = 80 KB |
| Per-client (1M clients) | Infeasible |
This is why systems like Dynamo used 'server-side' vector clocks (one slot per server, not per client), accepting reduced precision for practical size bounds.
Mitigation Strategies:
The terms 'vector clock' and 'version vector' are often used interchangeably, but there are subtle distinctions in their application.
In Practice:
The distinction is often academic. What matters is that both provide:
When reading database documentation, treat 'vector clock' and 'version vector' as roughly equivalent. The implementation details vary, but the core purpose—tracking causality for conflict detection—remains the same.
Riak calls them 'vector clocks,' Cassandra's documentation refers to 'version vectors,' CouchDB uses 'revision trees.' The concepts overlap significantly. Focus on understanding the semantics (causal tracking for conflict detection) rather than getting hung up on terminology.
When vector clocks detect concurrency, the system (or application) must resolve the conflict. Here's a complete implementation pattern.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
interface VectorClock { [nodeId: string]: number;} interface VersionedValue<T> { value: T; vclock: VectorClock;} type ConflictResolution<T> = | { type: 'resolved'; value: T } | { type: 'merged'; value: T } | { type: 'keep_both'; values: T[] }; interface ConflictResolver<T> { /** * Called when concurrent versions are detected. * @param versions All concurrent versions * @returns Resolution decision */ resolve(versions: VersionedValue<T>[]): ConflictResolution<T>;} /** * Vector clock-based conflict-detecting store. */class VectorClockStore<T> { private data: Map<string, VersionedValue<T>[]> = new Map(); private resolver: ConflictResolver<T>; constructor(resolver: ConflictResolver<T>) { this.resolver = resolver; } /** * Write a value. Detects conflicts with existing versions. */ write(key: string, value: T, vclock: VectorClock): void { const existing = this.data.get(key) ?? []; // Determine relationship with each existing version const dominated: VersionedValue<T>[] = []; const concurrent: VersionedValue<T>[] = []; for (const version of existing) { const comparison = this.compare(vclock, version.vclock); if (comparison === 'AFTER') { // New version supersedes this one dominated.push(version); } else if (comparison === 'CONCURRENT') { // Genuine conflict concurrent.push(version); } // If BEFORE, the existing version supersedes new (reject new) // If EQUAL, same version (skip) } // If new version is dominated by any existing, reject it const dominated_by_existing = existing.some( v => this.compare(vclock, v.vclock) === 'BEFORE' ); if (dominated_by_existing) { console.log('Write rejected: superseded by existing version'); return; } const newVersion: VersionedValue<T> = { value, vclock }; if (concurrent.length === 0) { // No conflicts: new version replaces dominated ones this.data.set(key, [newVersion]); } else { // Conflicts detected! Use resolver const allVersions = [...concurrent, newVersion]; const resolution = this.resolver.resolve(allVersions); switch (resolution.type) { case 'resolved': case 'merged': // Single resolved value with merged clock const mergedClock = this.mergeClock(allVersions.map(v => v.vclock)); this.data.set(key, [{ value: resolution.value, vclock: mergedClock }]); break; case 'keep_both': // Store as siblings for later resolution this.data.set(key, allVersions); break; } } } /** * Read value(s). May return multiple if unresolved siblings exist. */ read(key: string): VersionedValue<T>[] { return this.data.get(key) ?? []; } private compare(vc1: VectorClock, vc2: VectorClock): string { // (Implementation from earlier) const allNodes = new Set([...Object.keys(vc1), ...Object.keys(vc2)]); let v1Le = true, v2Le = true, anyDiff = false; for (const node of allNodes) { const v1 = vc1[node] ?? 0; const v2 = vc2[node] ?? 0; if (v1 > v2) v1Le = false; if (v2 > v1) v2Le = false; if (v1 !== v2) anyDiff = true; } if (!anyDiff) return 'EQUAL'; if (v1Le) return 'BEFORE'; if (v2Le) return 'AFTER'; return 'CONCURRENT'; } private mergeClock(clocks: VectorClock[]): VectorClock { const result: VectorClock = {}; for (const clock of clocks) { for (const [node, count] of Object.entries(clock)) { result[node] = Math.max(result[node] ?? 0, count); } } return result; }} // Example: Shopping cart merge resolverconst cartResolver: ConflictResolver<CartItems> = { resolve(versions) { // Merge strategy: union of all carts const merged: CartItems = { items: [] }; const itemMap = new Map<string, CartItem>(); for (const version of versions) { for (const item of version.value.items) { const existing = itemMap.get(item.id); if (!existing || item.addedAt > existing.addedAt) { itemMap.set(item.id, item); } } } merged.items = Array.from(itemMap.values()); return { type: 'merged', value: merged }; }};Vector clocks provide the theoretical foundation for detecting true concurrency in distributed systems. Unlike LWW, they tell us when we have a real conflict versus a simple ordering. Let's consolidate the key insights:
What's Next:
Vector clocks detect conflicts, but when a real conflict occurs, we need a strategy to resolve it. The next page explores Application-Level Merge—how to write domain-specific resolution logic that combines conflicting values intelligently, preserving as much user intent as possible.
You now understand vector clocks comprehensively—their structure, comparison semantics, production implementations, and practical limitations. You can reason about causal relationships in distributed systems and detect true concurrency.