Back to Scenarios & Design Questions
Q62Question 62 of 67

Why wall-clock timestamps fail for ordering, and how logical clocks fix it

Key Concepts
  • Physical clocks drift and are never perfectly synchronized, even with NTP
  • Sorting distributed events by wall-clock time can reverse cause and effect
  • Lamport clocks use a per-node counter to guarantee causal ordering but cannot detect concurrency
  • Vector clocks keep a counter per node to precisely detect concurrent vs causally-related events
  • Hybrid Logical Clocks combine physical time with a logical counter for monotonic, causal, human-readable timestamps
  • HLC is the standard choice for ordering financial events across services
  • Never trust raw wall-clock timestamps as the source of truth for ordering

Definition

Physical clocks drift and lie about order, so you order distributed events by causality using logical clocks: Lamport for order, vector for detecting concurrency, and HLC for monotonic, causally-correct, human-readable timestamps.
  • Wall-clock problem – independent machine clocks drift even with NTP, so an event that happened later can carry an earlier timestamp.
  • Lamport clock – a per-node counter that increments on events and takes the max on receive, guaranteeing causal ordering.
  • Vector clock – a counter per node that enables precise detection of concurrent vs causally-related events.
  • Hybrid Logical Clock (HLC) – combines a physical-time component with a logical counter for timestamps that are monotonic, causally consistent, and human-readable.
  • Rule – never sort financial events by raw wall-clock time; use logical or hybrid clocks instead.

Suppose two servers at National Bank of India each stamp a time on Rajesh's transactions using their own wristwatches. One watch runs three seconds fast, the other two seconds slow. Rajesh deposits, then withdraws, but the withdrawal server's slow watch stamps an earlier time than the deposit server's fast watch. Sorting by stamp time makes it look like he withdrew before he deposited, which could trigger a false overdraft. Logical clocks fix this by using a counter that always respects causality: the cause always gets a smaller number than the effect.

Understanding the Concepts

Why do wall-clock timestamps fail?

Every machine has its own hardware clock, and those clocks drift at different rates. NTP reduces skew to milliseconds in good conditions, but milliseconds of skew are enough to misorder events that happen close together. NTP can even step a clock backward, so a timestamp is not guaranteed to be monotonically increasing on a single node.

JAVA
package com.bank.clock;

public class ClockSkewDemo {

    public static void main(String[] args) {
        // Simulating two servers with skewed clocks
        long serverATime = System.currentTimeMillis() + 3000; // 3s fast
        long serverBTime = System.currentTimeMillis() - 2000; // 2s slow

        // Rajesh deposits on Server A, then withdraws on Server B
        long depositTimestamp = serverATime;    // 1719000003000
        long withdrawTimestamp = serverBTime;   // 1718999998000

        System.out.println("Deposit timestamp:  " + depositTimestamp);
        System.out.println("Withdraw timestamp: " + withdrawTimestamp);
        System.out.println("Withdraw < Deposit? " +
            (withdrawTimestamp < depositTimestamp));
        System.out.println(
            "Sorting by timestamp shows withdrawal BEFORE deposit!");
        System.out.println("This is WRONG: deposit caused the withdrawal.");
    }
}
TEXT
Deposit timestamp:  1719000003000
Withdraw timestamp: 1718999998000
Withdraw < Deposit? true
Sorting by timestamp shows withdrawal BEFORE deposit!
This is WRONG: deposit caused the withdrawal.

How do Lamport clocks fix ordering?

Each node keeps an integer counter. On any local event it increments. When sending a message it includes the counter. On receiving, it sets its counter to one more than the max of its own and the received value. This guarantees: if A causally happened before B, then A's Lamport timestamp is strictly less than B's.

JAVA
package com.bank.clock;

public class LamportClock {

    private int counter = 0;

    // Local event
    public synchronized int tick() {
        return ++counter;
    }

    // Send: include current counter in message
    public synchronized int send() {
        return ++counter;
    }

    // Receive: take max, then increment
    public synchronized int receive(int receivedTimestamp) {
        counter = Math.max(counter, receivedTimestamp) + 1;
        return counter;
    }

    public int getCounter() { return counter; }

    public static void main(String[] args) {
        LamportClock depositServer = new LamportClock();
        LamportClock withdrawServer = new LamportClock();

        // Rajesh deposits on server A
        int depositTs = depositServer.tick();      // 1
        System.out.println("Deposit Lamport: " + depositTs);

        // Server A sends event to Server B
        int sentTs = depositServer.send();          // 2

        // Server B receives and withdraws
        int receivedTs = withdrawServer.receive(sentTs); // max(0,2)+1 = 3
        System.out.println("Withdraw Lamport: " + receivedTs);

        System.out.println("Deposit (" + depositTs +
            ") < Withdraw (" + receivedTs + ")? " +
            (depositTs < receivedTs));
        System.out.println("Correct causal order maintained!");
    }
}
TEXT
Deposit Lamport: 1
Withdraw Lamport: 3
Deposit (1) < Withdraw (3)? true
Correct causal order maintained!

What are the limitations of Lamport clocks?

The converse is not true: a smaller Lamport number does not prove causality. Lamport clocks cannot tell you whether two events were truly concurrent or causally related.

TEXT
Lamport clock limitation:
  Event X on Node A: Lamport = 5
  Event Y on Node B: Lamport = 7

  X < Y in Lamport order, but:
  - Were they causally related? (X caused Y)
  - Or concurrent? (no causal link)
  Lamport cannot tell. You need vector clocks for that.

How do vector clocks detect concurrency?

Each node keeps a vector with one counter per node. On a local event it increments its own entry. On receiving, it takes the element-wise max and increments its own entry. Now you can compare precisely: if one vector is element-wise less-than-or-equal the first happened-before the second; if neither dominates they are concurrent.

JAVA
package com.bank.clock;

import java.util.Arrays;

public class VectorClock {

    private final int[] vector;
    private final int nodeId;

    public VectorClock(int nodeCount, int nodeId) {
        this.vector = new int[nodeCount];
        this.nodeId = nodeId;
    }

    public void tick() {
        vector[nodeId]++;
    }

    public int[] send() {
        vector[nodeId]++;
        return vector.clone();
    }

    public void receive(int[] received) {
        for (int i = 0; i < vector.length; i++) {
            vector[i] = Math.max(vector[i], received[i]);
        }
        vector[nodeId]++;
    }

    public static boolean happenedBefore(int[] a, int[] b) {
        boolean atLeastOneLess = false;
        for (int i = 0; i < a.length; i++) {
            if (a[i] > b[i]) return false;
            if (a[i] < b[i]) atLeastOneLess = true;
        }
        return atLeastOneLess;
    }

    public static boolean concurrent(int[] a, int[] b) {
        return !happenedBefore(a, b) && !happenedBefore(b, a)
            && !Arrays.equals(a, b);
    }

    @Override
    public String toString() {
        return Arrays.toString(vector);
    }

    public static void main(String[] args) {
        VectorClock nodeA = new VectorClock(2, 0);
        VectorClock nodeB = new VectorClock(2, 1);

        // Deposit on A
        nodeA.tick(); // [1, 0]
        System.out.println("Deposit on A: " + nodeA);

        // A sends to B
        int[] sent = nodeA.send(); // [2, 0]
        nodeB.receive(sent);        // [2, 1]
        System.out.println("Withdraw on B: " + nodeB);

        // Independent event on A
        nodeA.tick(); // [3, 0]
        System.out.println("Interest calc on A: " + nodeA);

        // Compare
        int[] deposit = {1, 0};
        int[] withdraw = {2, 1};
        int[] interest = {3, 0};

        System.out.println("Deposit -> Withdraw? " +
            happenedBefore(deposit, withdraw)); // true
        System.out.println("Interest concurrent with Withdraw? " +
            concurrent(interest, withdraw)); // true
    }
}
TEXT
Deposit on A: [1, 0]
Withdraw on B: [2, 1]
Interest calc on A: [3, 0]
Deposit -> Withdraw? true       (causal relationship)
Interest concurrent with Withdraw? true  (no causal link)

What is a Hybrid Logical Clock (HLC)?

HLC combines a physical-time component with a logical counter. The physical part stays close to real wall-clock time, and the logical counter breaks ties and preserves causality when physical time stalls or moves backward.

JAVA
package com.bank.clock;

public class HybridLogicalClock {

    private long physicalTime;
    private int logicalCounter;

    public HybridLogicalClock() {
        this.physicalTime = System.currentTimeMillis();
        this.logicalCounter = 0;
    }

    public synchronized long[] localEvent() {
        long wallClock = System.currentTimeMillis();
        if (wallClock > physicalTime) {
            physicalTime = wallClock;
            logicalCounter = 0;
        } else {
            logicalCounter++;
        }
        return new long[]{physicalTime, logicalCounter};
    }

    public synchronized long[] send() {
        return localEvent();
    }

    public synchronized long[] receive(long recvPhysical,
                                        int recvLogical) {
        long wallClock = System.currentTimeMillis();
        if (wallClock > physicalTime && wallClock > recvPhysical) {
            physicalTime = wallClock;
            logicalCounter = 0;
        } else if (recvPhysical > physicalTime) {
            physicalTime = recvPhysical;
            logicalCounter = recvLogical + 1;
        } else if (physicalTime == recvPhysical) {
            logicalCounter = Math.max(logicalCounter, recvLogical) + 1;
        } else {
            logicalCounter++;
        }
        return new long[]{physicalTime, logicalCounter};
    }

    public static void main(String[] args) {
        HybridLogicalClock hlc = new HybridLogicalClock();

        long[] ts1 = hlc.localEvent();
        System.out.printf("Event 1: physical=%d, logical=%d%n",
            ts1[0], ts1[1]);

        long[] ts2 = hlc.localEvent();
        System.out.printf("Event 2: physical=%d, logical=%d%n",
            ts2[0], ts2[1]);

        System.out.println("Both are monotonic, causal, and readable.");
    }
}
TEXT
Event 1: physical=1719000000000, logical=0
Event 2: physical=1719000000000, logical=1
Both are monotonic, causal, and readable.

HLC properties:
  Monotonic:   always increasing, never goes backward
  Causal:      respects happened-before ordering
  Readable:    physical part is close to wall-clock time
  Bounded:     logical counter stays small (close to real time)

How do the three compare?

| Feature | Lamport | Vector | HLC |
|---|---|---|---|
| Causal order | Yes | Yes | Yes |
| Detect concurrency | No | Yes | No |
| Human-readable | No | No | Yes |
| Close to real time | No | No | Yes |
| Memory per event | 1 integer | N integers | 1 long + 1 int |
| Best for | Simple ordering | Conflict detection | Financial events |

When should you use each clock?

Use Lamport when you only need a consistent causal total order. Use vector clocks when you must detect and resolve concurrent conflicts. Use HLC when you need causal ordering plus readable, close-to-real timestamps, which is the common choice for ordering financial events across services.

TEXT
Decision guide:
  Need causal order only?           -> Lamport clock
  Need to detect concurrent writes? -> Vector clock
  Need causal + readable time?      -> HLC (most common for banking)
  Need exact wall-clock time?       -> IMPOSSIBLE across nodes
  Never do this:                    -> Sort by raw wall-clock timestamp

What are common mistakes with event ordering?

The most common mistake is sorting distributed events by wall-clock timestamp. Another is assuming NTP provides perfect synchronization.

JAVA
package com.bank.clock;

import java.util.List;
import java.util.Comparator;

public class OrderingMistake {

    record Transaction(String type, long wallClockTs, int lamportTs) {}

    public static void main(String[] args) {
        var deposit = new Transaction("DEPOSIT", 1719000003000L, 1);
        var withdraw = new Transaction("WITHDRAW", 1718999998000L, 3);

        // WRONG: sort by wall clock
        List<Transaction> wrongOrder = List.of(deposit, withdraw).stream()
            .sorted(Comparator.comparingLong(Transaction::wallClockTs))
            .toList();
        System.out.println("Wrong order (wall clock): " +
            wrongOrder.get(0).type + " then " +
            wrongOrder.get(1).type);

        // CORRECT: sort by Lamport timestamp
        List<Transaction> correctOrder = List.of(deposit, withdraw).stream()
            .sorted(Comparator.comparingInt(Transaction::lamportTs))
            .toList();
        System.out.println("Correct order (Lamport): " +
            correctOrder.get(0).type + " then " +
            correctOrder.get(1).type);
    }
}
TEXT
Wrong order (wall clock): WITHDRAW then DEPOSIT  <- WRONG!
Correct order (Lamport): DEPOSIT then WITHDRAW   <- CORRECT!

Visual Overview

DIAGRAM
  Wall-Clock Problem:
  Server A (3s fast)          Server B (2s slow)
  ┌──────────────┐            ┌──────────────┐
  │ Deposit      │            │ Withdraw     │
  │ ts=10:00:03  │ ────────> │ ts=09:59:58  │
  └──────────────┘            └──────────────┘
  Sorting by ts: Withdraw (09:59:58) < Deposit (10:00:03)
  WRONG ORDER!  Deposit caused the withdrawal.

  Lamport Clock Fix:
  Server A                    Server B
  ┌──────────────┐            ┌──────────────┐
  │ Deposit      │  send(2)   │ Withdraw     │
  │ Lamport=1    │ ────────> │ Lamport=3    │
  └──────────────┘            └──────────────┘
  Sorting by Lamport: Deposit (1) < Withdraw (3)
  CORRECT ORDER!  Causality preserved.

  HLC Combines Both:
  ┌────────────────────────────────────┐
  │  HLC Timestamp = (physical, logic) │
  │  (1719000000000, 0) -> readable    │
  │  (1719000000000, 1) -> monotonic   │
  │  Causal + Close to wall-clock      │
  └────────────────────────────────────┘

Key Takeaway

You cannot order events across distributed nodes by wall-clock timestamps because every machine has its own hardware clock and those clocks drift. NTP reduces skew to milliseconds, but milliseconds are enough to misorder events, and NTP can even step a clock backward. In banking this could make a withdrawal appear before the deposit that funded it, triggering a false overdraft.

Logical clocks fix ordering by using causality instead of physical time. A Lamport clock is a per-node counter that guarantees if event A causally happened before event B, then A's counter is strictly less than B's. Vector clocks add the ability to detect concurrent events by keeping a counter per node. Hybrid Logical Clocks combine the best of both: a physical-time component that stays close to wall-clock time and a logical counter that preserves causality, producing timestamps that are monotonic, causally consistent, and human-readable.

At National Bank of India, Rajesh's deposit on one service triggered a withdrawal on another, and the two servers had skewed clocks. Sorting by wall clock made the withdrawal look earlier, which is clearly wrong. Using an HLC, the withdrawal's timestamp was forced above the deposit's regardless of clock skew, and an auditor could still read the approximate real time from the physical component.

Remember one sentence: physical clocks drift and can lie about order, so order distributed events by causality using Lamport, vector, or Hybrid Logical Clocks, and never sort financial events by raw wall-clock time.

---

Share
Use ← → arrow keys to navigate