Day 18 of 30 • Time & Ordering

Clocks, Ordering & Causality

Why distributed systems can't trust wall clocks. Lamport timestamps, vector clocks, and how Google Spanner uses atomic clocks.

Lamport Clocks Vector Clocks Causality TrueTime HLC

Why Wall Clocks Fail

⏰ Clock Skew

Each server has its own hardware clock. NTP synchronizes clocks but only to within 100ms–10ms accuracy typically. Two servers may disagree on the time by enough to completely reverse the causal order of events.

  • NTP accuracy: ±1ms datacenter, ±100ms internet
  • Clock drift: crystal oscillators drift ~1ms/day
  • Leap seconds: clocks can jump or repeat

💥 The Real Problem

A user posts a comment (Node A, t=100ms), then edits it (Node B, t=99ms due to clock skew). The system records the edit BEFORE the original post. Sorted by timestamp, you'd see the edit come first — violating causality.

Node A time: 10:00:00.100 — "Post comment"
Node B time: 10:00:00.099 — "Edit comment"
Sort by timestamp → Edit appears first!

Lamport Timestamps (1978)

📐 The Rules

Each node maintains a counter L. Three rules:

  1. Local event: L = L + 1
  2. Send message: L = L + 1, attach L to message
  3. Receive message with timestamp T: L = max(L, T) + 1

Key insight: if event A happened-before event B, then L(A) < L(B). But the converse is NOT true — L(A) < L(B) does NOT mean A happened before B. Lamport clocks don't detect concurrent events.

🕐 Lamport Clock Simulator

Three nodes (A, B, C) each with their own Lamport counter. Simulate sends and receives.

Vector Clocks — Detecting Concurrency

🔢 Vector Clock Rules

Each node maintains a vector V[n] (one counter per node). Rules:

  1. Local event: V[self]++
  2. Send: V[self]++, send V with message
  3. Receive with W: V[i] = max(V[i], W[i]) for all i, then V[self]++

🔍 Comparing Vector Clocks

A → B (A happened-before B) iff: V(A)[i] ≤ V(B)[i] for all i, and at least one is strictly less.

Concurrent (A ∥ B) iff: neither A → B nor B → A. Used by DynamoDB, Riak to detect conflicts.

Vector Clock Example (3 nodes: A, B, C)

A initial
[0,0,0]
B initial
[0,0,0]
C initial
[0,0,0]
A local event
[1,0,0]
B local event
[0,1,0]
A sends to B
[2,0,0]
B receives A
[2,2,0]
A event
[3,0,0]
B event
[2,3,0]
A ∥ B? YES (concurrent!)
[3,0,0] ∥ [2,3,0]

A's last event [3,0,0] and B's last event [2,3,0]: A has 3>2 in position 0, B has 3>0 in position 1. Neither dominates — concurrent!

Advanced: TrueTime & Hybrid Logical Clocks

⚛️ Google TrueTime (Spanner)

GPS + Atomic Clocks

Google's Spanner uses GPS receivers and atomic clocks in every datacenter. TrueTime API returns [earliest, latest] — a bounded interval of true wall-clock time. Spanner guarantees: if commit of T1 happens before start of T2, then T1.timestamp < T2.timestamp.

  • Uncertainty epsilon: 1–7ms
  • Commit waits until uncertainty passes
  • Enables externally consistent transactions

🔗 Hybrid Logical Clocks (HLC)

CockroachDB

HLC = max(wall_time, lamport_counter). Advances monotonically, preserves causality, stays close to wall clock. CockroachDB uses HLC for transaction timestamps — no atomic clocks needed, just NTP + a logical offset that accounts for skew.

  • l = physical component (NTP time)
  • c = logical counter for same millisecond
  • HLC timestamp: (l, c)

Implementation Examples

from threading import Lock
from typing import Optional

class LamportClock:
    """Thread-safe Lamport logical clock."""
    def __init__(self):
        self._time = 0
        self._lock = Lock()

    def tick(self) -> int:
        """Local event — increment and return new time."""
        with self._lock:
            self._time += 1
            return self._time

    def send(self) -> int:
        """Before sending a message — tick and return timestamp to attach."""
        return self.tick()

    def receive(self, msg_timestamp: int) -> int:
        """On receiving a message — update to max + 1."""
        with self._lock:
            self._time = max(self._time, msg_timestamp) + 1
            return self._time

class VectorClock:
    """Vector clock for causality tracking across N nodes."""
    def __init__(self, node_id: str, all_nodes: list[str]):
        self.node_id = node_id
        self.vector = {n: 0 for n in all_nodes}
        self._lock = Lock()

    def tick(self) -> dict:
        with self._lock:
            self.vector[self.node_id] += 1
            return dict(self.vector)

    def send(self) -> dict:
        return self.tick()

    def receive(self, incoming: dict) -> dict:
        with self._lock:
            for node, ts in incoming.items():
                self.vector[node] = max(self.vector.get(node, 0), ts)
            self.vector[self.node_id] += 1
            return dict(self.vector)

    def happens_before(self, a: dict, b: dict) -> bool:
        """Returns True if a -> b (a happened before b)."""
        all_lte = all(a.get(n, 0) <= b.get(n, 0) for n in set(a)|set(b))
        any_lt = any(a.get(n, 0) < b.get(n, 0) for n in set(a)|set(b))
        return all_lte and any_lt

    def concurrent(self, a: dict, b: dict) -> bool:
        return not self.happens_before(a, b) and not self.happens_before(b, a)

Comparison: Ordering Mechanisms

MechanismDetects Concurrent?OverheadUse Case
Wall Clock (NTP)No — unreliableNoneLogging, approximate ordering
Lamport TimestampsNo (partial order only)O(1) int per messageDistributed mutex, total ordering
Vector ClocksYesO(N) per messageConflict detection (DynamoDB, Riak)
Hybrid Logical ClocksPartialO(1), stays near wall timeCockroachDB, YugabyteDB
TrueTime (atomic)Yes (bounded)Hardware cost, 1–7ms waitGoogle Spanner — serializable globally

Knowledge Check

1. Node A has Lamport clock L=5. It receives a message with timestamp T=10. What is A's clock after receiving?

2. Vector clocks V(A) = [3,1,0] and V(B) = [2,3,0]. What is their relationship?

3. Why does Google Spanner "commit-wait" for the TrueTime uncertainty interval?

4. A Lamport clock shows event A has timestamp 5 and event B has timestamp 3. What can you conclude?

5. DynamoDB uses vector clocks (dotted version vectors) to detect conflicts. When two vector clocks are concurrent, what does DynamoDB do?

Day 18 Complete!

You now understand distributed time. Next: Failure Modes & Fault Tolerance.