Clocks and Ordering
Wall clocks jump, so systems order events with counters and causality; physical time returns only with a bound on its error.
performance.now() -> monotonic, for durations | Date.now() -> wall clock, for timestampsEvery machine has two clocks and they answer different questions. The time-of-day clock (CLOCK_REALTIME, Date.now()) tells you what a human calls now; NTP steers it, so it can jump forwards and backwards. The monotonic clock (CLOCK_MONOTONIC, performance.now()) only moves forward but has no meaning as an absolute time. Durations need the monotonic one, timestamps need the other, and mixing them is a real outage: at midnight UTC on 1 January 2017 Cloudflare's RRDNS computed a duration across the leap second from Go's wall clock, got a negative number, passed it to rand.Int63n and panicked. About 0.2% of DNS queries were affected.
Ordering without trusting time
Lamport clocks (1978) are a counter per node: increment it on every event, send it with every message, and on receipt set it above both your own value and the one received. That gives a total order consistent with causality — but from two Lamport timestamps you cannot tell whether the events were ordered or concurrent. Vector clocks can: one counter per node, compared element-wise, and neither dominating the other means concurrent. Dynamo attaches a version vector to each value and hands concurrent versions back to the caller rather than guessing between them.
Where physical time comes back
Spanner's TrueTime returns an interval rather than an instant, with uncertainty of roughly 1 to 7 ms in the published measurements, and commit-wait holds a transaction until that interval has passed its commit timestamp. Hybrid logical clocks are the commodity version — physical milliseconds plus a counter. CockroachDB uses them with --max-offset defaulting to 500 ms, and a node that drifts past 80% of that against half its peers shuts itself down.
Example
// Two clocks, two jobs. Using one for the other's job is the bug.
// --- durations: monotonic only ------------------------------------------
const t0 = performance.now(); // ms since time origin, never steps backwards
await handleRequest(req);
const elapsedMs = performance.now() - t0;
metrics.histogram("request.duration_ms").observe(elapsedMs);
// Node-only, same clock with nanosecond resolution and no float rounding:
const n0 = process.hrtime.bigint(); // bigint nanoseconds, monotonic
await handleRequest(req);
const elapsedNs = process.hrtime.bigint() - n0;
// --- timestamps: wall clock only ---------------------------------------
const stampedAt = Date.now(); // shorthand for the longhand below
const stampedAtLong = new Date().getTime(); // identical value, same clock
const iso = new Date(stampedAt).toISOString();
// The failure mode this prevents. If a duration is computed from the wall
// clock, an NTP step or a leap second can make it negative:
const wallStart = Date.now();
await handleRequest(req);
const wrong = Date.now() - wallStart; // CAN be negative
if (wrong < 0) {
// Cloudflare's 2017 outage was one of these reaching code that assumed a
// duration is never negative. Clamping hides it; using performance.now()
// means the situation cannot arise.
}
// Rule of thumb: if the number is going into a subtraction, use the monotonic
// clock. If it is going into a database column, a log line or a response body,
// use the wall clock - and never use it to resolve a write conflict.When to use it
- Deciding which timestamp to trust when two replicas disagree about the newest value of a key
- Measuring request latency in a process whose clock is being corrected by NTP
- Ordering messages in a chat system where clients submit their own timestamps
- Explaining to an interviewer how a system knows that one event caused another
More examples
A Lamport clock
The smallest correct answer to "how do you order events without synchronised clocks", and the basis of most sequencers and ID generators. Reach for it when you need a deterministic total order - deduplicating a log, tie-breaking a conflict - and do not need to know whether two events were concurrent.
// One counter per node. Gives a total order consistent with causality:
// if a happened before b, then C(a) < C(b). The converse does not hold.
class LamportClock {
constructor(nodeId) { this.nodeId = nodeId; this.counter = 0; }
// IR1: increment between any two successive events on this node.
tick() { return ++this.counter; }
// IR2: a message carries the sender's clock value.
send(payload) {
return { payload, ts: this.tick(), from: this.nodeId };
}
// On receive, move above both our own value and the one received.
receive(message) {
this.counter = Math.max(this.counter, message.ts) + 1;
return this.counter;
}
// Ties are broken by node id so the order is total rather than partial.
static compare(a, b) {
return a.ts - b.ts || (a.from < b.from ? -1 : a.from > b.from ? 1 : 0);
}
}
// What this cannot tell you: given ts=5 on node A and ts=7 on node B, whether
// B's event was caused by A's or merely happened to be numbered higher.Vector clocks, which can say "concurrent"
Shows the one thing timestamps can never express: that two writes genuinely conflicted and no automatic winner exists. Use it when silently dropping a write is unacceptable - cart contents, collaborative edits, offline-first clients - and you have an application-level merge rule.
// One counter per node, carried with the value. Comparing two vectors gives
// three answers rather than two, which is the point.
function increment(vector, nodeId) {
return { ...vector, [nodeId]: (vector[nodeId] ?? 0) + 1 };
}
function compare(a, b) {
const nodes = new Set([...Object.keys(a), ...Object.keys(b)]);
let aAhead = false, bAhead = false;
for (const n of nodes) {
const av = a[n] ?? 0, bv = b[n] ?? 0;
if (av > bv) aAhead = true;
if (bv > av) bAhead = true;
}
if (aAhead && bAhead) return "concurrent"; // neither dominates: a real conflict
if (aAhead) return "after";
if (bAhead) return "before";
return "equal";
}
// Dynamo-style read: return every concurrent version to the caller instead of
// silently choosing one. Shopping carts merge by union; a user profile may
// need the human to pick.
function reconcile(versions) {
return versions.filter((v) =>
!versions.some((other) => compare(v.clock, other.clock) === "before"));
}A hybrid logical clock
The practical middle ground: ordering you can trust and timestamps a human can read, without atomic clocks. Reach for it when events from several nodes land in one ordered stream and operators will later ask "what happened around 14:03?" of the same column the system sorts by.
// Kulkarni et al. (2014): physical milliseconds plus a counter, so timestamps
// stay close to wall-clock time AND never violate causality. This is the shape
// CockroachDB and MongoDB's clusterTime use.
class HybridLogicalClock {
constructor(physical = Date.now) { this.pt = physical; this.l = 0; this.c = 0; }
// Local event or send: take the max of our l and physical time. If l did
// not move, the physical clock has not advanced, so bump the counter.
send() {
const lPrev = this.l;
this.l = Math.max(lPrev, this.pt());
this.c = this.l === lPrev ? this.c + 1 : 0;
return { l: this.l, c: this.c };
}
// Receive: max of our l, the message's l, and physical time - then take the
// counter from whichever side the max came from.
receive({ l: lm, c: cm }) {
const lPrev = this.l;
this.l = Math.max(lPrev, lm, this.pt());
if (this.l === lPrev && this.l === lm) this.c = Math.max(this.c, cm) + 1;
else if (this.l === lPrev) this.c = this.c + 1;
else if (this.l === lm) this.c = cm + 1;
else this.c = 0;
return { l: this.l, c: this.c };
}
}
// Compare (l, c) lexicographically. Unlike a Lamport counter the value is
// still readable as a date, so it can go in a timestamp column - and unlike a
// raw Date.now() it cannot order an effect before its cause.
Discussion