This distinction is almost entirely absent from time standards, and it is the one that matters most for event sourcing and multi-agent systems.
Two events can carry UTC timestamps
A = 12:00:03
B = 12:00:02
while the truth is that A happened before B — because of queue delays, retries, clock skew, or distributed clocks. The canonical hour-of-week slot orders events on the wall clock; it does not, and cannot, order them causally. Physical order and causal order are different relations, and for correctness you often need the causal one.
Sometimes there is no order at all
Two events on independent nodes may have no happens-before relation — there is no fact about which came first. Forcing a UTC total order on them invents an order that is an artifact of clock skew, not causality.
A UTC timestamp always gives you a tiebreak. That is exactly the trap: it will happily order two concurrent events, and the order is meaningless. Use the wall clock for reporting; use a logical clock for ordering.
Three standard tools (executable here)
The KB ships Lamport clocks, vector clocks, and hybrid logical clocks as tested functions, so causal order is checkable, not just described.
import { vectorCompare, vectorTick, lamportTick, hlcLocal, hlcReceive, hlcCompare } from "@/lib/time";
// Vector clocks detect concurrency — the thing UTC cannot.
vectorCompare({ a: 1, b: 0 }, { a: 2, b: 0 }); // "before"
vectorCompare({ a: 1, b: 0 }, { a: 0, b: 1 }); // "concurrent" ← unordered
// Lamport gives a total order consistent with causality (no concurrency info).
lamportTick(4, [7, 2]); // 8 = max(local, received) + 1
// A hybrid logical clock (HLC) keeps causal order AND stays near physical time,
// even when a clock steps backwards (VM restore, NTP correction).
const a = hlcLocal({ physicalMs: 0, logical: 0 }, 1000); // sender at t=1000
const b = hlcReceive({ physicalMs: 990, logical: 0 }, a, 990); // receiver skewed low
hlcCompare(b, a) > 0; // true — b still sorts AFTER a despite the smaller clock
- Lamport — a monotonic counter,
max(seen) + 1. A total order consistent with causality, but it cannot tell you two events were concurrent. - Vector clocks — one counter per node.
vectorComparereturnsbefore,after,equal, orconcurrent, so concurrency is explicit. - Hybrid logical clocks — physical time plus a logical tiebreak; causal order that stays close to UTC and survives a backwards clock step.
The tested reference implementation is lib/time/causal.ts.
Where physical and causal time meet the slot
Slot the events on the wall clock for hour-of-week analysis and reporting, but keep a causal stamp for anything order-sensitive: queue reordering, duplicate replay (same event time, new ingestion), and event corrections that keep the occurrence time fixed while the value changes. The slot says when on the clock; the logical clock says in what order. A temporal-interoperability standard for agents and event streams has to carry both.
