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.

Vectors [A:2,B:1] and [A:1,B:2] are concurrent despite equal sums. Componentwise maximum merges context to [A:2,B:2], then B’s receive event yields [A:2,B:3]. Pruning C can erase a real ancestor relationship.
Concurrent histories need a policy. Source-based mechanism illustration. Model values and source-reported observations are not a new runtime test.

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