Tier 2 · Distributed Core

Clocks & Ordering

Ordering events without a global clock — Lamport, vectors, HLC, TrueTime

⏱ 18 min distributedorderingconsistency

Two kinds of clock, and the one you keep misusing

Time-of-day clock (System.currentTimeMillis, time.time()) returns wall-clock time synchronised to NTP. It can jump backwards when NTP corrects drift, when a leap second is smeared or applied, or when a VM is restored from a snapshot.

Monotonic clock (System.nanoTime, time.monotonic()) only ever moves forward. Its absolute value is meaningless — it's only useful as a difference.

The rule that follows:

How wrong are clocks, really?

With good NTP inside a datacenter: typically under a millisecond, occasionally tens of milliseconds. Across the public internet or on a busy VM: tens to hundreds of milliseconds. When NTP is misconfigured, firewalled, or the machine has just booted: seconds to years.

Nothing announces the drift. A machine with a clock 5 seconds fast looks entirely healthy and happily wins every last-write-wins race in the cluster.

This is why the replication lesson called last-write-wins a data loss policy: it doesn't resolve conflicts by recency, it resolves them by whose clock is furthest ahead.

Happens-before: ordering without clocks

Leslie Lamport's insight is to stop asking about time and ask about causality.

Event A happens-before B (written A → B) if:

If neither A → B nor B → A, the events are concurrent — not "at the same time", but causally independent. Nothing either one did could have influenced the other, so any order is equally valid.

That's the key reframing: you rarely need to know what time something happened. You need to know what it could have depended on.

Lamport timestamps

The minimal implementation:

  1. Each process keeps a counter.
  2. Increment before every event.
  3. Send the counter with every message.
  4. On receive: counter = max(local, received) + 1.

Now A → B implies L(A) < L(B). Total ordering, tiny overhead, one number.

The limitation: the converse doesn't hold. L(A) < L(B) does not mean A happened before B — they might be concurrent. Lamport timestamps give you a consistent total order, but they cannot detect concurrency, which is exactly what you need to know when resolving a conflict.

Vector clocks

Keep one counter per node instead of one overall. Node i increments its own entry on each event, and on receiving a message takes the element-wise max.

Now comparison is genuinely informative:

This is what Dynamo-style stores use to detect siblings and hand them to the application rather than silently discarding one. (In replication contexts they're usually called version vectors — same mechanism, tracking replicas rather than processes.)

The cost: size grows with the number of nodes, and entries for departed nodes need pruning. For a cluster of 5 that's trivial; for millions of mobile clients each acting as a replica, it's not.

Size Detects concurrency? Total order?
Wall clock 1 value No Yes, but wrong
Lamport 1 counter No Yes
Vector clock O(nodes) Yes Partial order
HLC 2 values Approximately Yes

Hybrid Logical Clocks

The practical compromise, and increasingly the default in modern databases.

An HLC is a pair: a physical component (close to wall-clock time) and a logical counter. It updates like a Lamport clock but keeps the physical part tracking real time within the bound of clock skew.

You get: causality preserved (if A → B then HLC(A) < HLC(B)), timestamps that are meaningful as approximate wall time — you can ask "what did this look like at 14:32" — and constant size. CockroachDB, MongoDB, and YugabyteDB all use HLCs.

TrueTime, and buying your way out

Google's Spanner takes the opposite approach: rather than working around clock uncertainty, measure and bound it.

TrueTime uses GPS receivers and atomic clocks in every datacenter, and its API doesn't return a timestamp — it returns an interval [earliest, latest] guaranteed to contain the true time. The uncertainty ε is typically a few milliseconds.

The trick is commit-wait: to commit a transaction, Spanner picks a timestamp and then deliberately waits out the uncertainty window — a few milliseconds of doing nothing — before making the write visible. That guarantees no other transaction can be assigned an overlapping timestamp, which makes timestamps globally meaningful and yields externally consistent (strictly serializable) transactions across continents.

Practical guidance

What to take away

Check yourself

  1. You measure how long an operation took by subtracting two System.currentTimeMillis() readings. What can go wrong?

  2. Lamport timestamps give L(A) < L(B). What does that tell you?

  3. What is the essential trick behind Spanner's TrueTime?

  4. Two replicas hold conflicting values and their version vectors are neither less than nor greater than one another. What does this mean?