Logical clocks order events in distributed systems based on causality, not wall-clock time. They answer: “Could event A have caused event B?” without requiring synchronized physical clocks. The two main types are Lamport clocks (single counter, partial ordering) and vector clocks (one counter per process, detects concurrency). For production systems, Hybrid Logical Clocks (HLC) combine the best of both worlds.
Visual Overview
Logical vs Physical Time
Logical vs Physical Time
THE PROBLEM WITH PHYSICAL TIME
┌────────────────────────────────────────────────────┐│││ Machine A clock: 10:00:01.000 ││ Machine B clock: 10:00:01.005 (5ms ahead!) ││││ Event X on A at 10:00:01.003 (A's clock) ││ Event Y on B at 10:00:01.002 (B's clock) ││││ Physical order: Y before X (wrong!) ││ Real causality: X might have caused Y ││││ Problems: ││ - Clocks drift between NTP syncs ││ - Network latency affects sync accuracy ││ - Leap seconds, daylight saving, timezone bugs │││└────────────────────────────────────────────────────┘
THE LOGICAL CLOCK SOLUTION
┌────────────────────────────────────────────────────┐│││ Don't ask: "What time did this happen?" ││ Ask: "Could event A have caused event B?" ││││Happens-Before (→): ││ - A → B if same process, A first ││ - A → B if A is send, B is matching receive ││ - A → B if A → C and C → B (transitive) ││││ If neither A → B nor B → A: events are CONCURRENT │││└────────────────────────────────────────────────────┘
Core Explanation
What are Logical Clocks?
Real-World Analogy: Imagine detectives reconstructing a crime timeline. They don’t have synchronized watches, but they know:
“The window was broken before the alarm went off” (same location, sequence)
“The alarm triggered the police dispatch” (cause and effect)
“The neighbor’s dog barked” (concurrent—maybe caused by crime, maybe not)
They’re building a causal graph, not a timeline. Logical clocks do the same for distributed events.
The Happens-Before Relation
Leslie Lamport defined the fundamental “happens-before” relation (→) in 1978:
Happens-Before Rules
Happens-Before Rules
RULE 1: LOCAL ORDERING
┌────────────────────────────────────────────────────┐│││ Same process, A occurs before B: ││││ Process P: ──A──────B──────────►││││ A → B (A happens-before B) │││└────────────────────────────────────────────────────┘
RULE 2: MESSAGE CAUSALITY
┌────────────────────────────────────────────────────┐│││ A sends message, B receives it: ││││ Process P: ──A────────────────►│││││└──msg───┐││↓││ Process Q: ─────────B─────────►││││ A → B (send happens-before receive) │││└────────────────────────────────────────────────────┘
RULE 3: TRANSITIVITY
┌────────────────────────────────────────────────────┐│││ If A → C and C → B, then A → B ││││ Process P: ──A────────────────►│││││└──msg───┐││↓││ Process Q: ─────────C──────────►│││││└──msg───┐││↓││ Process R: ──────────────────B───►││││ A → C and C → B, therefore A → B │││└────────────────────────────────────────────────────┘
CONCURRENT EVENTS (∥)
┌────────────────────────────────────────────────────┐│││ If neither A → B nor B → A: ││││ Process P: ──A─────────────────►││││ Process Q: ─────────B──────────►││││ A ∥ B (A is concurrent with B) ││ Neither could have influenced the other │││└────────────────────────────────────────────────────┘
The Family of Logical Clocks
Logical Clock Comparison
Logical Clock Comparison
LAMPORT CLOCKS
┌────────────────────────────────────────────────────┐│││ Single integer counter per process ││││ Space: O(1) per process ││ Guarantee: A → B implies L(A) < L(B) ││ Limitation: L(A) < L(B) does NOT imply A → B ││││ Example: [5] ││││ Best for: Total ordering, simple causality │││└────────────────────────────────────────────────────┘
VECTOR CLOCKS
┌────────────────────────────────────────────────────┐│││ Array of N counters (one per process) ││││ Space: O(N) per process ││ Guarantee: A → B implies V(A) < V(B) ││ V(A) < V(B) implies A → B ││ Can detect: A ∥ B (concurrent) ││││ Example: [3, 5, 2] (seen 3 from P1, 5 from P2...) ││││ Best for: Conflict detection, CRDTs │││└────────────────────────────────────────────────────┘
HYBRID LOGICAL CLOCKS (HLC)
┌────────────────────────────────────────────────────┐│││ Combines physical time + logical counter ││││ Space: O(1) per process ││ Guarantee: Bounded drift from physical time ││ Causality preserved ││ Useful for time-based queries ││││ Example: (physical_ts, logical_counter) ││ (1704067200, 3) ││││ Best for: Production databases (CockroachDB, Spanner) │││└────────────────────────────────────────────────────┘
Vector Clocks Deep Dive
Vector Clock Execution
Vector Clock Execution
VECTOR CLOCK RULES
┌────────────────────────────────────────────────────┐│││ N processes, each maintains vector of N counters ││ VC[i] = "my count of events from process i" ││││ 1. LOCAL EVENT: VC[self]++ ││ 2. SEND: VC[self]++, attach VC to message ││ 3. RECEIVE: VC = max(VC, msg.VC); VC[self]++ │││└────────────────────────────────────────────────────┘
VECTOR CLOCK EXAMPLE (3 processes)
┌────────────────────────────────────────────────────┐│││ P1: ─●──────────●────────────────────●──────►││ [1,0,0] [2,0,0] [3,2,0] ││││↑││││ send receive │││↓│││ P2: ──│────────●────────●───────────●───────►│││ [1,1,0] [1,2,0] │││↑│ send │││ receive ↓││││││ P3: ──│──────────────────●──────────────────►│││ [1,2,1] │││↑│││ receive ││↓││││ Compare [2,0,0] and [1,2,0]: ││ 2 > 1 but 0 < 2 → CONCURRENT! (neither ≤ other) │││└────────────────────────────────────────────────────┘
VECTOR CLOCK COMPARISON RULES
┌────────────────────────────────────────────────────┐│││ VC1 < VC2 (A caused B) if: ││ ∀i: VC1[i] ≤ VC2[i] AND ∃j: VC1[j] < VC2[j] ││││ VC1 = VC2 (same event) if: ││ ∀i: VC1[i] = VC2[i] ││││ VC1 ∥ VC2 (concurrent) if: ││ Neither VC1 < VC2 nor VC2 < VC1 ││ (some elements larger, some smaller) ││││ Examples: ││ [2,3,1] < [2,4,2] (all ≤, some <) → causality ││ [3,1,2] ∥ [2,3,1] (3>2 but 1<3) → concurrent │││└────────────────────────────────────────────────────┘
Comparison Table
Aspect
Lamport Clock
Vector Clock
HLC
Space per process
O(1)
O(N)
O(1)
Message overhead
1 integer
N integers
2 integers
Causality: A→B implies clock(A) < clock(B)
✓
✓
✓
Detects concurrency
✗
✓
✗
Bounded from physical time
✗
✗
✓
Time-range queries
✗
✗
✓
Real Systems Using Logical Clocks
System
Clock Type
Use Case
Notes
CockroachDB
HLC
Transaction ordering
Bounded clock skew
Google Spanner
TrueTime (+ logical)
Global transactions
GPS + atomic clocks
Riak
Dotted Version Vectors
Sibling detection
Vector clock lineage
TiDB
HLC variant
MVCC timestamps
Similar to CockroachDB
Note: Some systems historically associated with vector clocks (e.g., Dynamo) have evolved. DynamoDB’s current implementation differs from the 2007 paper. Cassandra uses physical timestamps with last-write-wins, not Lamport clocks. Always verify current behavior in documentation.
Hybrid Logical Clocks (HLC)
Hybrid Logical Clock
Hybrid Logical Clock
HLC STRUCTURE
┌────────────────────────────────────────────────────┐│││ HLC = (physical_time, logical_counter) ││││ physical_time: Wall clock time (with NTP) ││ logical_counter: Distinguishes events at same time││││ Property: HLC.physical is bounded from actual time││ (Unlike Lamport, which can drift arbitrarily far) │││└────────────────────────────────────────────────────┘
HLC RULES
┌────────────────────────────────────────────────────┐│││ send/local event: ││ pt = max(hlc.pt, now()) ││ if pt == hlc.pt: ││ hlc.lc += 1 ││ else: ││ hlc.lc = 0 ││ hlc.pt = pt ││││ receive(msg): ││ pt = max(hlc.pt, msg.hlc.pt, now()) ││ if pt == hlc.pt == msg.hlc.pt: ││ hlc.lc = max(hlc.lc, msg.hlc.lc) + 1 ││ elif pt == hlc.pt: ││ hlc.lc += 1 ││ elif pt == msg.hlc.pt: ││ hlc.lc = msg.hlc.lc + 1 ││ else: ││ hlc.lc = 0 ││ hlc.pt = pt │││└────────────────────────────────────────────────────┘
WHY HLC MATTERS
┌────────────────────────────────────────────────────┐│││ Problem with pure Lamport: ││ Timestamp 100000 might be from 1 minute ago ││ Can't do "give me events from last 5 minutes" ││││ HLC solution: ││ HLC (1704067200, 5) ≈ Jan 1, 2024 00:00:00 ││ Can do time-range queries! ││ But still preserves causality ││││ Used by: CockroachDB, TiDB, YugabyteDB │││└────────────────────────────────────────────────────┘
When to Use What
✓ Decision Guide
Choosing a Logical Clock
Choosing a Logical Clock
DECISION FLOWCHART
┌────────────────────────────────────────────────────┐│││ Need to detect concurrent writes? │││││├── YES → Vector Clocks (or DVV) │││ (Dynamo, Riak) │││││└── NO → Need time-range queries? │││││├── YES → Hybrid Logical Clocks │││ (CockroachDB, Spanner) │││││└── NO → Lamport Clocks ││ (Simple ordering) │││└────────────────────────────────────────────────────┘
USE CASE MAPPING
┌────────────────────────────────────────────────────┐│││Use Case│ Best Clock ││──────────────────────┼────────────────────────││ Log correlation │ Lamport ││ Total order broadcast │ Lamport + tie-breaker ││ Shopping cart merge │ Vector ││ CRDT synchronization │ Vector ││ Database transactions │ HLC ││ Time-series data │ HLC │││└────────────────────────────────────────────────────┘
✕ Common Mistakes
Logical Clock Anti-patterns
Logical Clock Anti-patterns
MISTAKE 1: Vector clocks with many processes
Problem: 1000 processes = 1000 integers per message
Better: Use version vectors with pruning, or HLC
MISTAKE 2: Lamport when you need conflict detection
Problem: Can't tell if two writes are concurrent
Better: Vector clocks for merge/conflict scenarios
MISTAKE 3: Assuming clock comparison equals causality
Problem: Lamport: clock(A) < clock(B) ≠ A→B
Better: Understand that < is necessary but not sufficient
MISTAKE 4: Using physical time for ordering
Problem: Clock skew, drift, leap seconds
Better: Logical clocks for causality, HLC for time + causality
Interview Application
Common Interview Question
Q: “Compare Lamport clocks and vector clocks. When would you use each?”
Strong Answer:
“Both are logical clocks for ordering events without synchronized physical clocks, but they have different capabilities:
Lamport Clocks:
Single integer per process
Guarantee: If A→B (A caused B), then L(A) < L(B)
Limitation: L(A) < L(B) does NOT mean A→B (might be concurrent)
Space: O(1) per process
Vector Clocks:
Array of N integers (one per process)
Guarantee: If A→B then V(A) < V(B), AND vice versa
Capability: Can detect concurrent events (A ∥ B)
Space: O(N) per process
When to use Lamport:
Total order broadcast (just need consistent ordering)
“Vector clock comparison determines causality or concurrency:
V1 < V2 (V1 happened before V2):
All elements of V1 ≤ corresponding elements of V2, AND at least one is strictly less.
V1 = V2:
All elements are equal (same event or same causal history).
V1 ∥ V2 (concurrent):
Neither V1 < V2 nor V2 < V1. Some elements are larger in V1, some in V2.
Example:
[2, 3, 1] vs [2, 4, 2]2≤2, 3≤4, 1≤2, and at least one < → [2,3,1] < [2,4,2] (causal)[3, 1, 2] vs [2, 3, 1]3>2 but 1<3 → concurrent (neither is "before" the other)
This comparison is what enables conflict detection—if two writes are concurrent, you need application-level merge logic.”
Follow-up: What are Hybrid Logical Clocks and why do databases use them?
“HLC combines physical timestamps with logical counters:
Structure: (physical_time, logical_counter)
Why they exist:
Lamport clocks can drift arbitrarily far from physical time. If you’re at Lamport time 1,000,000, you have no idea if that was 1 second or 1 hour ago. This makes time-range queries impossible.
HLC properties:
Preserves causality like Lamport
Physical component stays bounded from wall clock
Logical counter handles ties at same physical time
Use case:
‘Give me all transactions from the last 5 minutes’ — only works if timestamps relate to real time.
Used by: CockroachDB, TiDB, YugabyteDB.
Trade-off: HLC doesn’t detect concurrency like vector clocks. For conflict detection, you still need vector clocks or explicit conflict markers.”
Code Example
Vector Clock Implementation (Python)
from typing import Dict, Optionalfrom copy import deepcopyclass VectorClock: """ Vector clock for detecting causality and concurrency. Each process maintains a vector of counters, one per process. """ def __init__(self, process_id: str, known_processes: list[str] = None): self.process_id = process_id self.clock: Dict[str, int] = {} if known_processes: for p in known_processes: self.clock[p] = 0 self.clock[process_id] = 0 def increment(self) -> 'VectorClock': """Increment own counter (local event or send).""" self.clock[self.process_id] = self.clock.get(self.process_id, 0) + 1 return self def update(self, other: 'VectorClock') -> 'VectorClock': """ Update clock on receive. Takes element-wise max, then increments own counter. """ for process_id, count in other.clock.items(): self.clock[process_id] = max( self.clock.get(process_id, 0), count ) self.increment() return self def copy(self) -> 'VectorClock': """Create a copy of this clock.""" new_clock = VectorClock(self.process_id) new_clock.clock = deepcopy(self.clock) return new_clock def __lt__(self, other: 'VectorClock') -> bool: """ Check if self happened-before other. Returns True if all elements ≤ and at least one <. """ all_processes = set(self.clock.keys()) | set(other.clock.keys()) all_leq = True any_lt = False for p in all_processes: self_val = self.clock.get(p, 0) other_val = other.clock.get(p, 0) if self_val > other_val: all_leq = False if self_val < other_val: any_lt = True return all_leq and any_lt def __eq__(self, other: 'VectorClock') -> bool: """Check if clocks are equal.""" all_processes = set(self.clock.keys()) | set(other.clock.keys()) return all( self.clock.get(p, 0) == other.clock.get(p, 0) for p in all_processes ) def is_concurrent(self, other: 'VectorClock') -> bool: """Check if self and other are concurrent (neither caused the other).""" return not (self < other) and not (other < self) and not (self == other) def compare(self, other: 'VectorClock') -> str: """Compare two clocks and describe relationship.""" if self == other: return "equal" elif self < other: return "self happened-before other" elif other < self: return "other happened-before self" else: return "concurrent" def __repr__(self) -> str: return f"VC({dict(self.clock)})"# Usage exampleif __name__ == "__main__": print("=== Vector Clock Demo ===\n") # Three processes processes = ["A", "B", "C"] a = VectorClock("A", processes) b = VectorClock("B", processes) c = VectorClock("C", processes) # A does local event a.increment() print(f"A local event: {a}") # A sends to B msg_clock = a.copy() a.increment() print(f"A sends to B: {a}") # B receives from A b.update(msg_clock) print(f"B receives: {b}") # B does local event b.increment() print(f"B local event: {b}") # Meanwhile, C does local event (concurrent with A and B) c.increment() print(f"C local event: {c}") # Compare clocks print(f"\nComparing A and B: {a.compare(b)}") print(f"Comparing A and C: {a.compare(c)}") print(f"Comparing B and C: {b.compare(c)}") print("\n=== Conflict Detection Example ===\n") # Simulate two concurrent writes write1 = VectorClock("client1", ["client1", "client2"]) write2 = VectorClock("client2", ["client1", "client2"]) write1.increment() # [1, 0] write2.increment() # [0, 1] print(f"Write 1: {write1}") print(f"Write 2: {write2}") print(f"Relationship: {write1.compare(write2)}") if write1.is_concurrent(write2): print("CONFLICT DETECTED: Need application-level resolution")