A vector records progress for each tracked participant. Comparing these counters distinguishes causal order from concurrency; the application still decides how to reconcile concurrent values.
One timestamp is not enough
A scalar timestamp can impose an order, but that order alone cannot prove causality. With stable participant identities and the usual event-clock rules, each process increments its own component for a local event. Receiving a message merges components by maximum and then increments the receiver’s component. These rules record information flow. They do not measure elapsed time or establish a physical-time ordering between machines that exchanged no information.
Compare vectors to see causality
Compare every component, treating an absent participant as zero. A is strictly before B only when all A components are at most B and at least one is smaller. Reverse domination means after; identical components mean equal. Only when neither vector dominates the other are they concurrent. Because a causal order accumulates everywhere the two paths crossed, a vector captures the transitive and direct edges that a single scalar time forfeits. The moment a message passes from A to B, B’s vector reflects both B’s own progress and everything A had seen, which is how the model traces causality instead of guessing it.
Concurrent means someone decides
Concurrency does not select a winner or necessarily imply a business conflict. Independent field updates might merge; incompatible replacements might require retaining siblings or asking a user. A deterministic tie-breaker can deliberately discard one value, but that is an application policy, not causality discovered by the clock. Equality means equal represented context; only an additional event-identity contract makes it proof that two payloads are the same event.
When it breaks
Clock size grows with the identities tracked by the chosen scheme. In Dynamo, components identify coordinating storage nodes, not every client that ever touched a key. Ephemeral participant identities and counter reuse after restart need explicit handling: reusing an old identity with a reset counter can make genuinely new work look obsolete. Stable identity is part of the algorithm’s assumptions, not an incidental implementation detail.
Pruning a component can lose a real ancestor relationship. Compare ancestor [A:2, C:4] with descendant [A:3, B:2, C:5]. Remove C from the descendant and the remaining vector now appears concurrent with the ancestor. Missing-as-zero is meaningful for an unseen participant; it cannot restore deliberately discarded history. Dynamo explicitly notes reconciliation imprecision from truncation. Dotted version vectors use a different causal representation; they are not permission to delete arbitrary components safely.
Comparing only sums, maxima, or nearby wall-clock values also loses information. [A:2, B:1] and [A:1, B:2] have equal sums but are concurrent. Carry the read context into a subsequent write so the store knows which versions it supersedes. Omitting that context can create siblings depending on the store and current versions; it does not guarantee a new sibling on every write. Retention and reconciliation require the store’s actual protocol.
Check it quickly
Save this as check_vector_clocks.py and run python3 check_vector_clocks.py. This in-memory comparison model covers all four relations, a merge followed by a receive-event increment, and a pruning counterexample. It creates no network, store, or persistent participant identity.
def compare(a, b):
"""Compare causal contexts with absent participants interpreted as zero."""
nodes = set(a) | set(b)
le = all(a.get(n, 0) <= b.get(n, 0) for n in nodes)
ge = all(a.get(n, 0) >= b.get(n, 0) for n in nodes)
if le and ge:
return "equal"
if le:
return "before"
if ge:
return "after"
return "concurrent"
def merge(a, b):
return {n: max(a.get(n, 0), b.get(n, 0)) for n in set(a) | set(b)}
def tick(clock, node):
result = dict(clock)
result[node] = result.get(node, 0) + 1
return result
a = {"A": 1}
b = tick(a, "B")
assert compare(a, b) == "before" and compare(b, a) == "after"
assert compare(a, a) == "equal"
assert compare({"A": 1}, {"A": 1, "B": 0}) == "equal"
assert a == {"A": 1} # tick does not mutate the input
c, d = {"A": 2, "B": 1}, {"A": 1, "B": 2}
assert compare(c, d) == compare(d, c) == "concurrent"
assert sum(c.values()) == sum(d.values()) # equal sums do not mean equal history
m = merge(c, d)
assert m == {"A": 2, "B": 2}
assert compare(c, m) == compare(d, m) == "before"
received = tick(m, "B")
assert received == {"A": 2, "B": 3} and compare(m, received) == "before"
# Truncation loses a genuine ancestor relationship.
ancestor = {"A": 2, "C": 4}
descendant = {"A": 3, "B": 2, "C": 5}
pruned = {"A": 3, "B": 2}
assert compare(ancestor, descendant) == "before"
assert compare(ancestor, pruned) == "concurrent"
print("PASS: four relations, merge-then-tick, zero slots, and false concurrency after pruning")
All assertions pass on Python 3.12.14. The merged concurrent histories are [A:2, B:2]; recording B’s receive event produces [A:2, B:3]. The pruning test changes a genuine before relation into concurrent, demonstrating a concrete loss of information rather than merely observing a shorter vector.
The model assumes valid nonnegative counters and a shared identity scheme. It does not test crash recovery, membership churn, client/server context exchange, or dotted versions. Those are separate integration properties. Passing comparison arithmetic cannot prove that a distributed store increments, persists, or retires its metadata correctly.
Further reading
Sources
- Dynamo: Amazon’s highly available key-value store, reconciliation with version vectors and sibling retention, checked 2026-10-15.
- Preguiça et al.: Dotted Version Vectors, causal histories with bounded server identities and explicit update dots, checked 2026-10-15.