logical and vector clocks
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.
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 |
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.
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.
| 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.
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 |
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
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
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.
| 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";
}
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 |
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"
}
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.
| 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 |
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.
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 |
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)
)
};
}
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
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
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.
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.
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.
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.
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.