Table of Contents

    logical and vector clocks

    SYSTEM DESIGN • CHAPTER 14.5

    Logical and Vector Clocks

    Understand why wall-clock timestamps cannot order distributed events, how logical clocks capture causality instead, and how vector clocks distinguish genuine concurrency from a sequence you simply observed out of order.

    Learning objective: By the end of this article, you will understand the happens-before relation, Lamport clocks and their limitation, vector clocks and concurrency detection, version vectors for replicated data, hybrid logical clocks, and how each choice affects conflict resolution.

    Prerequisites

    Recommended Knowledge

    • Partial failure and network uncertainty
    • Replication and replica divergence
    • Consistency models, particularly causal consistency
    • Conflict detection and resolution
    • Message passing between services
    • Event ordering in queues and logs
    • Idempotency and duplicate handling
    • Basic set and comparison operations

    Why Wall Clocks Fail

    The intuitive approach to ordering distributed events is to stamp each with the local time and sort. This fails for reasons that are physical rather than implementational.

    Problem Cause Consequence
    Clock skew Independent oscillators drift apart Nodes disagree about the current instant
    Synchronization error Network delay is asymmetric and variable Correction itself carries uncertainty
    Backward jumps Clock adjusted after drifting ahead Later events receive earlier timestamps
    Leap second handling Implementations differ across systems Duplicate or non-monotonic timestamps
    Virtualization pauses Guest suspended by the host Clock resumes with a sudden gap
    Insufficient resolution Events closer than the tick interval Identical timestamps for ordered events
    The Real Damage A write made later in real time can carry an earlier timestamp than one it should supersede. Last-writer-wins resolution then discards the newer value while appearing to work correctly.

    Simple Analogy

    Photographs from several cameras whose internal clocks were never synchronized. Sorting by timestamp produces a plausible-looking sequence that misrepresents what actually happened first.

    Physical time answers when something happened according to one machine. Distributed systems need to know what happened before what, which is a different question entirely.

    The Happens-Before Relation

    Rather than asking about absolute time, we ask whether one event could possibly have influenced another. This defines a partial order over events.

    HAPPENS-BEFORE
    \[ A \rightarrow B \]
    Rule Condition Reasoning
    Program order A precedes B on the same node Sequential execution establishes order
    Message order A sends a message, B receives it Receipt cannot precede sending
    Transitivity A precedes B and B precedes C Influence propagates through the chain

    Concurrency Means Independence

    If neither event happens before the other, they are concurrent. This is a statement about causal independence, not about simultaneous occurrence.

    CONCURRENT EVENTS
    \[ A \parallel B \iff \neg(A \rightarrow B) \wedge \neg(B \rightarrow A) \]
    Concurrent does not mean simultaneous: Two events an hour apart are concurrent if neither node knew of the other's event. Causality, not the wall clock, determines the classification.

    Lamport Clocks

    A Lamport clock is a single counter per node, maintained by two simple rules. It is the minimal mechanism that respects happens-before.

    Event Action
    Local event or send Increment the counter
    Message receive Take the maximum of local and received, then increment
    RECEIVE RULE
    \[ C_{\text{local}} = \max(C_{\text{local}}, C_{\text{message}}) + 1 \]
    class LamportClock {
        constructor(nodeId) {
            this.nodeId = nodeId;
            this.counter = 0;
        }
    
        tick() {
            this.counter += 1;
            return this.counter;
        }
    
        send() {
            return {
                nodeId: this.nodeId,
                timestamp: this.tick()
            };
        }
    
        receive(messageStamp) {
            this.counter = Math.max(
                this.counter,
                messageStamp.timestamp
            ) + 1;
    
            return this.counter;
        }
    
        compare(other) {
            if (this.counter !== other.timestamp) {
                return this.counter - other.timestamp;
            }
    
            return this.nodeId.localeCompare(other.nodeId);
        }
    }

    The Guarantee and Its Limit

    ONE-WAY IMPLICATION
    \[ A \rightarrow B \;\Rightarrow\; C(A) < C(B) \]

    The converse does not hold. A smaller counter does not prove causal precedence, because two independent nodes can reach the same counter values without any interaction.

    What Lamport Clocks Give

    • A total order consistent with causality
    • Constant space per node
    • Trivial comparison of two stamps
    • Suitable for deterministic tie-breaking

    What They Cannot Do

    • Detect whether events were concurrent
    • Prove that A actually influenced B
    • Support meaningful conflict detection
    • Distinguish divergence from a sequence
    THE CRITICAL LIMITATION
    Ordering everything is not the same as understanding causality. A Lamport clock will happily impose an order on two conflicting writes, hiding the conflict rather than revealing it.

    Vector Clocks

    A vector clock replaces the single counter with one counter per node. Each node tracks what it knows about every other node's progress, which is enough to detect concurrency.

    VECTOR STRUCTURE
    \[ V = [c_1, c_2, \ldots, c_n] \]
    Event Action
    Local event Increment own position only
    Send message Increment own position, attach the full vector
    Receive message Element-wise maximum, then increment own position
    class VectorClock {
        constructor(nodeId, initial = {}) {
            this.nodeId = nodeId;
            this.vector = { ...initial };
            this.vector[nodeId] = this.vector[nodeId] ?? 0;
        }
    
        tick() {
            this.vector[this.nodeId] += 1;
            return { ...this.vector };
        }
    
        merge(incoming) {
            const nodes = new Set([
                ...Object.keys(this.vector),
                ...Object.keys(incoming)
            ]);
    
            for (const node of nodes) {
                this.vector[node] = Math.max(
                    this.vector[node] ?? 0,
                    incoming[node] ?? 0
                );
            }
    
            this.vector[this.nodeId] += 1;
            return { ...this.vector };
        }
    }

    Comparing Two Vectors

    Relationship Condition Interpretation
    A before B Every element of A ≤ B, at least one strictly less A causally precedes B
    B before A Every element of B ≤ A, at least one strictly less B causally precedes A
    Equal All elements identical The same event
    Concurrent Neither dominates the other Independent, a genuine conflict
    function compareVectors(a, b) {
        const nodes = new Set([
            ...Object.keys(a),
            ...Object.keys(b)
        ]);
    
        let aGreater = false;
        let bGreater = false;
    
        for (const node of nodes) {
            const av = a[node] ?? 0;
            const bv = b[node] ?? 0;
    
            if (av > bv) aGreater = true;
            if (bv > av) bGreater = true;
        }
    
        if (aGreater && bGreater) return "concurrent";
        if (aGreater)             return "a-after-b";
        if (bGreater)             return "b-after-a";
    
        return "equal";
    }
    The Decisive Capability When neither vector dominates, the system knows with certainty that two writes were made independently. That is a real conflict requiring resolution, not an ordering to guess at.

    Worked Comparison

    Consider two replicas that both update a record while partitioned from each other.

    Step Node A Vector Node B Vector Detectable?
    Initial shared state {A:1, B:1} {A:1, B:1} Identical
    A writes during partition {A:2, B:1} {A:1, B:1} A is ahead
    B writes during partition {A:2, B:1} {A:1, B:2} Concurrent, conflict detected
    After merge {A:3, B:2} {A:3, B:2} Converged
    What a Lamport Clock Would Report Both writes carry some counter value, so one appears to follow the other. The system silently discards a valid update and reports success.

    Version Vectors

    Version vectors apply the same principle to replicated data items rather than to events. Counters are keyed by replica, and the vector travels with the value.

    Aspect Vector Clock Version Vector
    Tracks Events across processes Versions of a data item
    Keyed by Process identifier Replica identifier
    Incremented on Any local event Update to that item
    Primary use Causal event ordering Replica conflict detection
    Stored with Messages The value itself
    {
        "key": "cart:user-4821",
        "siblings": [
            {
                "value": { "items": ["sku-501", "sku-733"] },
                "versionVector": { "replica-east": 4, "replica-west": 2 },
                "writtenAt": "2026-09-24T05:31:02Z"
            },
            {
                "value": { "items": ["sku-501", "sku-910"] },
                "versionVector": { "replica-east": 3, "replica-west": 3 },
                "writtenAt": "2026-09-24T05:31:07Z"
            }
        ],
        "conflictDetected": true,
        "resolutionRequired": "application"
    }
    Siblings are a feature, not a failure: Returning both concurrent versions lets the application apply domain knowledge. A cart can merge item sets, whereas an arbitrary timestamp comparison would lose one customer's addition.

    The Cost of Vectors

    Vector clocks carry a real cost that grows with participant count, which is the principal reason they are not used universally.

    SPACE COMPLEXITY
    \[ S_{\text{vector}} = O(N) \]
    Concern Problem Mitigation
    Vector growth One entry per participating node Key by replica, not by client
    Stale entries Departed nodes remain in the vector Prune entries past a threshold
    Storage overhead Vector attached to every value Compact encoding and dictionary compression
    Network overhead Vector sent with every message Delta encoding between known states
    Sibling accumulation Unresolved conflicts multiply Cap siblings and resolve eagerly
    The Client-Keyed Mistake Keying vectors by client identifier causes unbounded growth, since every client that ever wrote adds a permanent entry. Keying by replica bounds the vector to the cluster size.

    Hybrid Logical Clocks

    Logical clocks capture causality but produce values meaningless to humans and unusable for time-range queries. Hybrid logical clocks combine physical time with a logical counter.

    HYBRID STRUCTURE
    \[ HLC = (T_{\text{physical}}, C_{\text{logical}}) \]
    class HybridLogicalClock {
        constructor() {
            this.physical = 0;
            this.logical = 0;
        }
    
        now() {
            const wall = Date.now();
    
            if (wall > this.physical) {
                this.physical = wall;
                this.logical = 0;
            } else {
                this.logical += 1;
            }
    
            return { physical: this.physical, logical: this.logical };
        }
    
        update(remote) {
            const wall = Date.now();
            const maxPhysical = Math.max(
                wall,
                this.physical,
                remote.physical
            );
    
            if (maxPhysical === this.physical &&
                maxPhysical === remote.physical) {
                this.logical = Math.max(this.logical, remote.logical) + 1;
            } else if (maxPhysical === this.physical) {
                this.logical += 1;
            } else if (maxPhysical === remote.physical) {
                this.logical = remote.logical + 1;
            } else {
                this.logical = 0;
            }
    
            this.physical = maxPhysical;
            return { physical: this.physical, logical: this.logical };
        }
    }

    Advantages

    • Respects causality like a logical clock
    • Stays close to wall-clock time
    • Supports time-bounded queries
    • Readable in logs and debugging
    • Constant size per timestamp

    Limitations

    • Still cannot detect concurrency
    • Accuracy depends on clock synchronization
    • Large skew inflates the logical component
    • Not a substitute for version vectors

    Choosing a Mechanism

    Mechanism Detects Concurrency Size Appropriate For
    Physical timestamp No Constant Display and rough ordering only
    Lamport clock No Constant Deterministic total ordering
    Hybrid logical clock No Constant Causal order with readable time
    Vector clock Yes Linear in nodes Causal debugging and event analysis
    Version vector Yes Linear in replicas Replicated data conflict detection
    Dotted version vector Yes Bounded by replicas Many clients per replica
    SELECTION RULE
    If you must detect conflicts, you need a vector. If you only need a deterministic order, a scalar clock suffices at far lower cost.

    Resolving Detected Conflicts

    Detection is only half the work. Once concurrency is identified, the application must decide what the correct outcome is.

    Strategy Approach Suitable Data
    Union merge Combine both sets Shopping carts, tags, memberships
    Field-level merge Take each field from its later writer Profiles with independent fields
    Domain rule Business logic selects a winner Order status transitions
    Conflict-free type Merge is mathematically defined Counters, sets, collaborative text
    Preserve siblings Return both to the caller Documents needing human judgment
    Highest timestamp Later wall clock wins Only where loss is genuinely acceptable
    function resolveCartConflict(siblings) {
        const mergedItems = new Map();
        const removedItems = new Set();
    
        for (const sibling of siblings) {
            for (const item of sibling.value.items) {
                mergedItems.set(item.sku, {
                    sku: item.sku,
                    quantity: Math.max(
                        mergedItems.get(item.sku)?.quantity ?? 0,
                        item.quantity
                    )
                });
            }
    
            for (const sku of sibling.value.removed ?? []) {
                removedItems.add(sku);
            }
        }
    
        for (const sku of removedItems) {
            mergedItems.delete(sku);
        }
    
        return {
            items: Array.from(mergedItems.values()),
            removed: Array.from(removedItems),
            versionVector: mergeVectors(
                siblings.map(s => s.versionVector)
            )
        };
    }
    Deletion needs explicit tracking: Merging sets naively resurrects deleted items, since absence is indistinguishable from removal. Record removals explicitly, often as tombstones.

    Causal Tracing in Practice

    Beyond conflict detection, causal metadata is valuable for understanding what actually happened across services during an incident.

    {
        "eventId": "evt-9f41",
        "service": "order-service",
        "hlc": { "physical": 1790005872341, "logical": 3 },
        "causedBy": ["evt-8c17", "evt-8c22"],
        "traceId": "trace-4b2f",
        "operation": "order.confirmed"
    }

    What Causal Metadata Enables

    • Reconstructing true event order across services
    • Distinguishing cause from coincidence in incidents
    • Detecting out-of-order processing in consumers
    • Identifying which write a state resulted from
    • Validating that dependencies were respected

    Common Pitfalls

    Pitfall Consequence Correction
    Timestamp-based conflict resolution Silent loss of valid writes Use version vectors
    Client-keyed vectors Unbounded vector growth Key by replica
    Assuming Lamport detects conflicts Conflicts hidden by imposed order Understand the one-way implication
    Never pruning departed nodes Vectors grow permanently Prune with a retention policy
    Ignoring returned siblings Application picks arbitrarily Implement explicit merge logic
    Merging without tombstones Deleted items reappear Track deletions explicitly
    Comparing vectors from different keys Meaningless comparison result Scope vectors to a single item

    Monitoring

    Signals Worth Tracking

    • Conflict detection rate per key space
    • Sibling count distribution
    • Keys exceeding a sibling threshold
    • Vector size distribution and growth trend
    • Entries pruned during maintenance
    • Conflicts resolved automatically versus manually
    • Clock skew between nodes
    • Logical counter inflation in hybrid clocks
    • Out-of-order events detected downstream
    A conflict rate of zero in an eventually consistent system usually means conflicts are being silently discarded, not that none are occurring.

    Common Design Mistakes

    Weak Design

    • Ordering distributed events by wall clock
    • Using last-writer-wins on meaningful data
    • Assuming synchronized clocks
    • Keying version vectors by client
    • Discarding siblings without inspection
    • Letting vectors grow without bound
    • Merging collections without tombstones
    • Never measuring the conflict rate

    Strong Design

    • Uses causal metadata for ordering
    • Detects concurrency before resolving
    • Treats clocks as approximate
    • Keys vectors by replica
    • Implements domain-aware merge logic
    • Prunes stale vector entries
    • Tracks deletions explicitly
    • Monitors conflicts and sibling growth

    System Design Interview Discussion

    Question What Your Answer Should Cover
    Why not use timestamps? Skew, drift, and silent write loss
    What is happens-before? The three rules and partial ordering
    Why are Lamport clocks insufficient? One-way implication, no concurrency detection
    How do vector clocks detect conflicts? Neither vector dominating the other
    What is the cost? Linear space and metadata overhead
    How do you bound vector growth? Replica keying and pruning policy
    What happens after detection? Domain-specific merge strategies
    Why hybrid logical clocks? Causality with human-readable time

    Design Checklist

    Production Checklist

    • Never order distributed writes by wall clock alone
    • Decide explicitly whether conflict detection is needed
    • Use version vectors where writes may diverge
    • Key vectors by replica rather than by client
    • Define a pruning policy for departed nodes
    • Cap sibling count per key and alert on breaches
    • Implement domain-specific merge functions
    • Track deletions with tombstones
    • Set a retention policy for tombstones
    • Consider hybrid logical clocks for readable ordering
    • Propagate causal metadata across service boundaries
    • Monitor conflict rate and vector size
    • Alert on clock skew exceeding tolerance
    • Test concurrent writes during injected partitions
    • Verify merge functions are commutative and associative

    Knowledge Check

    1

    Why can wall clocks not order distributed events?

    Independent clocks drift, synchronization carries error, and adjustments can move time backwards, so a later write may carry an earlier timestamp.

    2

    What does the happens-before relation capture?

    Whether one event could have influenced another, through program order, message passing, or transitivity. Unrelated events are concurrent.

    3

    What is the Lamport clock limitation?

    Causal precedence implies a smaller counter, but the converse does not hold, so concurrency cannot be distinguished from genuine ordering.

    4

    How do vector clocks identify a conflict?

    When neither vector is element-wise less than or equal to the other, each node saw something the other did not, proving independent writes.

    5

    Why key version vectors by replica?

    Keying by client adds a permanent entry for every writer, growing without bound. Replica keying bounds the vector to the cluster size.

    Summary

    Physical clocks cannot order events across machines, because drift, synchronization error, and backward adjustments mean timestamps do not reliably reflect what happened first. Building conflict resolution on them silently discards valid writes.

    The happens-before relation replaces absolute time with causal influence, defined through program order, message passing, and transitivity. Events unrelated by this relation are concurrent, regardless of how far apart they occurred.

    Lamport clocks provide a total order consistent with causality using a single counter, but the implication runs only one way. They cannot distinguish concurrency, so they impose an order on conflicts rather than revealing them.

    Vector clocks and version vectors track one counter per node or replica, making concurrency detectable when neither vector dominates. This costs space linear in participants, which is why keying by replica and pruning stale entries matter. Hybrid logical clocks offer causal ordering with readable timestamps but still cannot detect conflicts.

    Key Takeaway

    Detect concurrency before resolving it. If two writes were genuinely independent, no timestamp comparison can tell you which is correct. Use version vectors where divergence is possible, key them by replica, implement merge logic that reflects your domain, and treat a zero conflict rate as a sign of silent data loss rather than success.