Monotonic clocks
At 02:14 IST on a Sunday morning, Karan at MealRush is on-call for the dispatch service that pairs riders with delivery agents. The pager fires: "rider-pickup latency p99 alarm — 41,000 ms". Forty-one seconds. He pulls the trace and finds a single span where the recorded duration is -3.2 seconds. The same trace, the same request id, on a healthy host. Negative duration. He does not yet know it, but the line that produced it reads duration = time.time() - start, where start was captured 90 seconds before chronyd issued a step correction at 02:13:11. The dispatcher's "elapsed time" gauge is meaningless until the next deploy. The fix is two characters — replacing time.time() with time.monotonic(). The reason that fix works is the subject of this chapter.
A monotonic clock is the kernel's guarantee that consecutive readings never decrease — the inverse promise of the wall clock. It is read from the same hardware counter (TSC, kvm-clock, HPET) that backs CLOCK_REALTIME, but without the NTP offset and without leap-second handling. Use it for every interval measurement on a single machine: timeouts, retries, deadlines, profiling, rate limiting, RTT estimation. Do not use it for cross-machine ordering — every host's monotonic origin is different. Two clocks, two jobs.
What CLOCK_MONOTONIC actually is at the kernel boundary
When you call time.monotonic() in Python, the C library resolves the call to clock_gettime(CLOCK_MONOTONIC, &ts). The kernel implements this by reading the same hardware counter it uses for CLOCK_REALTIME — typically the TSC on bare-metal x86, or kvm-clock inside a KVM guest — applying the same mult >> shift calibration to convert ticks to nanoseconds, and then stopping. There is no NTP offset added, no wall_to_monotonic baseline, no leap-second adjustment. The value returned is (hardware_ticks * mult / shift) — counter time in nanoseconds since some kernel-chosen origin.
The origin matters only in that it is fixed for the life of the boot. The kernel exports CLOCK_MONOTONIC as nanoseconds-since-boot, plus a small offset to ensure the value never starts at zero (which would make ratio-based code accidentally reciprocate). On a freshly booted Linux host the value at process start is typically a few seconds into the future — the kernel adds a monotonic_to_bootbase constant that varies slightly by kernel version and architecture but is always positive. What is guaranteed is the delta — the difference between two clock_gettime(CLOCK_MONOTONIC) calls is always non-negative and measures actual elapsed wall-clock time as accurately as the underlying hardware counter allows.
The kernel's monotonicity guarantee is enforced by a piece of code in kernel/time/timekeeping.c called timekeeping_update_from_shadow. Before publishing a new value of mult (for instance after NTP has computed a new frequency correction), the kernel computes what the next counter reading would yield and compares it to the last published reading. If the new calibration would cause the published time to go backwards — which can happen when mult is reduced — the kernel adjusts the baseline to ensure the next read is at least equal to the last. NTP can change the frequency, but the kernel will never publish a smaller monotonic value than it published a nanosecond ago. The guarantee is structural, not statistical.
Why the kernel cannot just make CLOCK_REALTIME monotonic and be done with it: the wall clock has an external contract — applications expect time.time() to roughly equal the wall-clock time printed on a NIST atomic clock. If NTP detects a 200 ms positive offset and the kernel refused to apply the correction in the wall direction, the wall clock would drift away from UTC indefinitely. The two clocks exist precisely because the two contracts are incompatible: one promises agreement with external UTC, the other promises non-decreasing local progress, and no single value can satisfy both. The cost is two API names and the cognitive load of choosing the right one.
Three monotonic clocks, not one — MONOTONIC, MONOTONIC_RAW, BOOTTIME
Linux exposes three closely-related monotonic clocks that differ in two dimensions: whether they include NTP frequency adjustments, and whether they advance during system suspend. Most production code wants CLOCK_MONOTONIC; the other two exist for narrow, well-defined cases.
CLOCK_MONOTONIC is the default. It is non-decreasing, includes NTP frequency adjustments (so a long interval is measured in "real" UTC nanoseconds, not raw quartz nanoseconds), and does not advance during system suspend. If your laptop sleeps for an hour, CLOCK_MONOTONIC will show almost no time elapsed during that hour. This is sometimes what you want (timing CPU work) and sometimes catastrophic (a 4-hour suspend that broke a 30-second deadline timer the application set before suspending — the deadline never fires until the thread wakes up plus 30 more wall-clock seconds).
CLOCK_MONOTONIC_RAW is the same as CLOCK_MONOTONIC but without NTP adjustments. The hardware counter is converted to nanoseconds using only the manufacturer-stated calibration, with no software correction. If your quartz crystal is running 50 ppm fast, CLOCK_MONOTONIC_RAW will report 50 ppm more elapsed time than reality. This is what you want when measuring the frequency stability of the hardware itself, or when you specifically want to bypass the NTP daemon (rare, but useful for benchmarking the daemon's correction quality). For normal application timing, the NTP frequency correction is helpful, not harmful — it makes intervals measured against the host more accurate, not less.
CLOCK_BOOTTIME is the same as CLOCK_MONOTONIC but does advance during suspend. If your laptop sleeps for an hour, CLOCK_BOOTTIME will show 1 hour of elapsed time during that hour; CLOCK_MONOTONIC will show seconds. This is what you want for any timer that should fire after wall-clock duration regardless of system state — wake-from-sleep alarms, certificate-renewal timers, "session expired after 30 minutes of real time even if the device was asleep" timers. Python 3.7+ exposes this as time.monotonic() on Linux when the kernel supports it (most do); on macOS and BSD the underlying clock is roughly equivalent. On older systems, time.monotonic() may map to CLOCK_MONOTONIC and silently fail to advance during suspend — a footgun the language documentation does not flag clearly.
Measuring monotonicity with a multi-clock probe
The easiest way to internalise the three clocks' differences is to read all of them in lockstep and compare. The script below samples CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_MONOTONIC_RAW, and CLOCK_BOOTTIME for 20 seconds at 200 Hz, computes per-clock skew, and flags any sample where one clock disagrees with another by more than 1 ms in a single tick. On a quiet host you will see all four clocks tracking each other within microseconds; if you chronyc makestep mid-sample, only CLOCK_REALTIME flinches. If you suspend the laptop briefly with systemctl suspend && sleep 5, only CLOCK_BOOTTIME keeps advancing.
# probe_monotonic.py — multi-clock sampler with per-clock skew analysis
import ctypes, ctypes.util, time, statistics
CLOCK_REALTIME, CLOCK_MONOTONIC = 0, 1
CLOCK_MONOTONIC_RAW, CLOCK_BOOTTIME = 4, 7
class TS(ctypes.Structure):
_fields_ = [("tv_sec", ctypes.c_long), ("tv_nsec", ctypes.c_long)]
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
libc.clock_gettime.argtypes = [ctypes.c_int, ctypes.POINTER(TS)]
def now_ns(clock_id: int) -> int:
ts = TS()
libc.clock_gettime(clock_id, ctypes.byref(ts))
return ts.tv_sec * 1_000_000_000 + ts.tv_nsec
def probe(duration_s=20.0, hz=200):
period = 1.0 / hz
samples = []
last = {c: now_ns(c) for c in (CLOCK_REALTIME, CLOCK_MONOTONIC,
CLOCK_MONOTONIC_RAW, CLOCK_BOOTTIME)}
deadline = last[CLOCK_MONOTONIC] + int(duration_s * 1e9)
while now_ns(CLOCK_MONOTONIC) < deadline:
cur = {c: now_ns(c) for c in last}
deltas = {c: cur[c] - last[c] for c in last}
flag = ""
if deltas[CLOCK_REALTIME] < 0:
flag = "WALL-BACKWARDS"
if deltas[CLOCK_MONOTONIC] < 0:
flag = "MONO-BACKWARDS-IMPOSSIBLE"
spread = max(deltas.values()) - min(deltas.values())
if spread > 1_000_000: # > 1 ms tick-to-tick spread
flag = flag or f"SPREAD-{spread//1000}us"
samples.append((deltas, flag))
last = cur
time.sleep(period)
return samples
if __name__ == "__main__":
s = probe()
flagged = [x for x in s if x[1]]
print(f"samples: {len(s)} flagged: {len(flagged)}")
for ev, flag in flagged[:5]:
print(f" {flag}: realtime={ev[CLOCK_REALTIME]/1e6:.2f}ms "
f"mono={ev[CLOCK_MONOTONIC]/1e6:.2f}ms "
f"raw={ev[CLOCK_MONOTONIC_RAW]/1e6:.2f}ms "
f"boot={ev[CLOCK_BOOTTIME]/1e6:.2f}ms")
Sample run on a quiet laptop:
samples: 4000 flagged: 0
Sample run on the same laptop, with sudo chronyc makestep invoked at t=8s:
samples: 4000 flagged: 1
WALL-BACKWARDS: realtime=-86.42ms mono=5.01ms raw=5.01ms boot=5.01ms
Read the flagged line carefully. The wall clock moved backwards by 86 milliseconds in a single 5 ms sample. The three monotonic clocks all advanced by the expected ~5 ms — they did not see the step. This is the property the script is demonstrating: CLOCK_MONOTONIC is structurally immune to NTP step corrections, where CLOCK_REALTIME is not.
Why the script uses raw clock_gettime via ctypes instead of time.monotonic(): Python's time module exposes only time.time() (CLOCK_REALTIME) and time.monotonic() (typically CLOCK_MONOTONIC, but may be CLOCK_BOOTTIME on Linux 3.5+ depending on Python version). To compare all four clocks in one process you have to drop down to the syscall directly — there is no time.boottime() or time.monotonic_raw() in the standard library. This is the reason a probe like this is rare in production code: most engineers learn about the difference only when they hit a bug, by which point reading the cpython source for "what does time.monotonic actually call" is the second item in their stack of half-finished investigations.
Why the spread metric matters more than any individual clock's value: tick-to-tick disagreement between any two clocks is the operational signal. If CLOCK_REALTIME and CLOCK_MONOTONIC agree to within microseconds across 4000 samples, the host's NTP daemon is healthy and the wall clock is being slewed gently — no harm done. If they disagree by milliseconds in a single sample, one of them just stepped, and the difference between the two reveals which: a step on the wall clock shows up in CLOCK_REALTIME but not the monotonic clocks; a hardware-counter glitch (very rare on modern TSC) would show up in all four. The spread is the diagnostic; the absolute values are noise.
A production incident — KapitalKite's order-timeout regression after a kernel upgrade
KapitalKite runs the order-routing layer for retail equity trades on a 12-node fleet of c6i.4xlarge instances. Each incoming order acquires a deadline of 800 ms — beyond which the order is rejected back to the user with "order routing timeout, please retry". Deadlines are tracked with deadline_mono = time.monotonic() + 0.8, and the routing loop checks if time.monotonic() > deadline_mono: reject() after each downstream call. The system had run cleanly for 18 months with deadline-rejection rates under 0.05% during normal market hours. On Wednesday, 17 September 2025, six of the twelve nodes started rejecting roughly 14% of orders within four minutes of market open. The other six were normal.
The cause was a kernel upgrade from 5.15 to 6.1 on the six affected nodes, applied during the previous night's maintenance window. The upgrade silently changed Python's time.monotonic() behaviour from CLOCK_MONOTONIC to CLOCK_BOOTTIME-equivalent on these specific kernel versions through a subtle interaction with the runtime's syscall selection logic. In practice it should not have mattered — both clocks advance at the same rate during normal operation. Except these instances had been part of a host migration two days before, and the source hypervisor had been suspended for 47 minutes during the migration window. On the migrated guests, CLOCK_BOOTTIME was 47 minutes ahead of CLOCK_MONOTONIC — the boot-time clock had counted the suspend period; the regular monotonic clock had not.
This 47-minute discrepancy was invisible during normal operation. It became visible at market open when the deadline-tracking code interacted with another piece of logic that was also using monotonic time but reading it differently. The metrics pipeline used psutil.Process.create_time() — which reads /proc/<pid>/stat field 22 (the process start time, in clock ticks since boot) — to compute "process age" for an alarm. The metrics code had been written assuming time.monotonic() - process_age == process_start_monotonic. After the upgrade, time.monotonic() was 47 minutes ahead of /proc/<pid>/stat's notion of boot time, and the resulting "process age" calculation was wrong by 47 minutes. That alone was a metric anomaly, not an outage.
The outage came from a third interaction: the routing layer's circuit breaker tripped if the rate of "process age vs monotonic time" disagreement exceeded a 1-second threshold over a 30-second window. The breaker tripped on the six migrated hosts. Tripped breakers caused the routing layer to enter "fallback" mode, which used a slower code path with an additional 300 ms RPC hop for risk validation. The slower path consumed enough of the 800 ms budget that 14% of orders genuinely could not complete in time on the affected hosts.
The fix was to standardise the monotonic-clock semantics across all code paths in the service. The patch (a) replaced every time.monotonic() with an explicit clock_gettime(CLOCK_BOOTTIME) for deadline tracking that should survive suspends, and clock_gettime(CLOCK_MONOTONIC) for measuring CPU work that should not, (b) added a startup-time assertion that CLOCK_MONOTONIC and CLOCK_BOOTTIME agree to within 1 second (failing loudly if a host has been suspended), and (c) downgraded the metrics-vs-monotonic alarm from "trip the breaker" to "page on-call". The post-incident review estimated the incident cost ₹62 lakh in retail-trader rejected orders during a 4-minute window of high volatility on the two largest-cap index constituents, and a follow-on engineering directive forbade using time.monotonic() anywhere in the codebase without an explicit comment naming which underlying clock was intended.
The deeper lesson — and the reason this incident appears in the curriculum — is that "monotonic" is not a single semantic. There are at least three monotonic clocks in Linux, plus a fourth on macOS (mach_absolute_time), plus a fifth on Windows (QueryPerformanceCounter). They differ in suspend behaviour, NTP-frequency adjustment, and what they call their origin. The cross-platform language abstraction time.monotonic() papers over the difference, and that paper tears the moment a host suspends mid-execution.
Common confusions
-
"
time.monotonic()is alwaysCLOCK_MONOTONICon Linux." It is on Python 3.5+ for most kernel versions, but the underlying mapping has changed across both kernel versions and Python's runtime — at various points it has beenCLOCK_MONOTONIC,CLOCK_MONOTONIC_RAW, or aCLOCK_BOOTTIME-equivalent. The KapitalKite incident above turns on this. If you need a specific clock's semantics, callclock_gettimedirectly viactypesand name the clock id. -
"Monotonic time gives me cross-machine ordering." It does not. Each host's
CLOCK_MONOTONICstarts at a different value and advances at a slightly different rate (within the quartz crystal's tolerance, plus or minus NTP's frequency correction). Subtracting two hosts' monotonic readings gives you nothing useful. For cross-machine ordering you need logical clocks (Lamport, vector, hybrid) or PTP-synchronised hardware. Monotonic time is a strictly single-machine primitive. -
"
time.monotonic()is slow." Aclock_gettime(CLOCK_MONOTONIC)call on modern Linux x86 with TSC takes about 5 nanoseconds — it goes through the vDSO and never enters the kernel proper. A program that calls it 10 million times in a tight loop spends about 50 ms in the call itself. The cost of monotonic time is essentially free; the only situation where it matters is hot inner loops in language runtimes (the Go runtime caches it, the Rust standard library caches it viaInstant::now, Python does not, which adds about 200 ns per call due to attribute lookup). -
"Monotonic time is exact." Monotonic time is non-decreasing, but it is not exact. The hardware counter has the same 50-ppm quartz tolerance as
CLOCK_REALTIME. A 1-hour interval measured byCLOCK_MONOTONIC_RAWcould be off by 180 ms purely due to crystal drift.CLOCK_MONOTONICis corrected by NTP's frequency adjustments, so it is closer to UTC duration, but it still inherits whatever residual error NTP has. For ±100 µs interval accuracy, you need PTP-grade hardware regardless of which clock you read. -
"I should always use
CLOCK_BOOTTIMEbecause it survives suspends." Often, you do not want it to. A profiling timer that should measure how long your function ran while the CPU was active should useCLOCK_MONOTONIC— if the laptop suspends mid-function, the elapsed CPU time is genuinely small even though the wall-clock duration is large. A session-expiry timer that should fire after 30 real-world minutes regardless of suspend should useCLOCK_BOOTTIME. The choice depends on what "time" means in the protocol you are implementing. -
"Monotonic clocks make timeouts safe." They make single-host timeout durations safe — you can subtract
time.monotonic()values without worrying about NTP step corrections producing negative durations. They do not solve coordinator-side timeouts in distributed protocols, where the timeout must be communicated across hosts, and the receiving host's clock is irrelevant. A 5-second RPC timeout is enforced by the sender, who measures elapsed time on its local monotonic clock; the receiver does not see the timeout's "absolute deadline" because there is no shared frame of reference for "absolute" between the two.
Going deeper
What the vDSO does and why monotonic time costs 5 nanoseconds
The Linux vdso (virtual Dynamic Shared Object) is a small shared library the kernel maps into every process's address space. It contains a handful of functions — clock_gettime, gettimeofday, time, getcpu — implemented in user-space using direct hardware access to a kernel-published memory page. When you call clock_gettime(CLOCK_MONOTONIC), glibc dispatches to the vDSO version, which reads the current TSC value via the rdtsc instruction, multiplies by a mult factor read from the kernel-published page, adds an offset, and returns. There is no syscall — no context switch, no kernel mode transition. The whole sequence is roughly 15–20 instructions and takes ~5 ns on a modern x86 CPU. The kernel updates the vDSO page every tick (~1 ms) under a seqlock so user-space readers see consistent values without locking. This is why "monotonic time is too expensive" is essentially never the right diagnosis for a production performance issue — the cost is rounding error compared to almost any other operation in your program. Where the cost does show up is in language runtimes that lock on every reading (Python's GIL adds ~100 ns; Java's System.nanoTime adds 50 ns of bookkeeping); in those cases the language overhead dominates the syscall, and the clock itself is not the bottleneck.
The TSC-stability flags and what cloud providers do
Modern x86 CPUs publish two CPU flags relevant to monotonic time: constant_tsc (the TSC ticks at a fixed rate independent of the current CPU frequency or P-state) and nonstop_tsc (the TSC keeps ticking through HALT and other idle states). Together they make TSC suitable as a system-wide monotonic clock source. On bare-metal x86 hardware from 2010 onward, both flags are essentially always set. On virtualised cloud instances the situation is more complex: AWS Nitro instances expose constant_tsc and nonstop_tsc to the guest because the underlying hardware has them and Nitro's TSC offset is stable across migrations. GCP and Azure are similar on their modern instance families. But not every cloud-provider VM is so well-behaved: if you check grep tsc /proc/cpuinfo on a low-cost shared-tenancy VM, you may find one flag set and not the other, in which case the kernel falls back to kvm-clock or HPET. This is why the Linux kernel's clock-source selection happens at boot — it inspects the CPU's flags and picks the most reliable monotonic source, with the choice persisting until reboot or until the hypervisor flips the flags during a host migration.
Why Java's System.currentTimeMillis() is wall-clock and System.nanoTime() is monotonic
The Java standard library names this distinction more honestly than C or Python. System.currentTimeMillis() is documented as returning "the difference, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC" — explicitly a wall-clock value, with the caveat that the value can decrease across NTP corrections. System.nanoTime() is documented as returning "the current value of the running Java Virtual Machine's high-resolution time source, in nanoseconds. This method can only be used to measure elapsed time and is not related to any other notion of system or wall-clock time." The Java documentation is explicit that subtracting two nanoTime() values is the only valid operation. The OpenJDK implementation on Linux maps nanoTime to clock_gettime(CLOCK_MONOTONIC) — the same clock Python's time.monotonic() resolves to most of the time. The lesson from the Java naming: the two clocks are different enough that they deserve different verbs in the standard library. C's clock_gettime(clock_id, ...) and Python's time.time() / time.monotonic() are both worse names for the same distinction, but the underlying semantics are identical.
Cross-language quirks: macOS, Windows, FreeBSD
Outside Linux, the monotonic-clock primitive is named differently and sometimes behaves differently. On macOS, the canonical monotonic source is mach_absolute_time(), which returns CPU-cycle ticks; you convert to nanoseconds via mach_timebase_info. macOS also exposes clock_gettime(CLOCK_MONOTONIC_RAW) as a POSIX shim, which calls into the same Mach primitive. On Windows, QueryPerformanceCounter is the monotonic primitive, with QueryPerformanceFrequency giving the conversion factor (typically 10 MHz on modern hardware). Windows additionally exposes GetTickCount64 (millisecond-resolution monotonic since boot) and GetSystemTimeAsFileTime (wall-clock as 100-nanosecond ticks since 1601-01-01). On FreeBSD, clock_gettime(CLOCK_MONOTONIC) is broadly equivalent to Linux's, but the CLOCK_UPTIME clock has slightly different suspend semantics (it advances during suspend on FreeBSD). The cross-platform pattern: every modern OS has a monotonic primitive, all of them work, and all of them are named differently and use different epochs. The cross-platform abstraction in language standard libraries (std::chrono::steady_clock in C++, Instant in Rust, time.monotonic() in Python, time.Now() in Go's monotonic-tagged time) is what most application code should use — but if you cross OS boundaries you must verify the underlying clock's suspend semantics.
Reproduce this on your laptop
# Reproduce the multi-clock probe and observe a step correction
python3 -m venv .venv && source .venv/bin/activate
python3 probe_monotonic.py &
sleep 5
sudo chronyc makestep # force a wall-clock step; monotonic is unaffected
wait
# Inspect your kernel's clock-source choice
cat /sys/devices/system/clocksource/clocksource0/available_clocksource
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
# See your CPU's TSC stability flags
grep -E "constant_tsc|nonstop_tsc" /proc/cpuinfo | head -1
# Measure the cost of a single monotonic call (should be ~5-50 ns)
python3 -c "
import time
n = 1_000_000
t0 = time.monotonic_ns()
for _ in range(n): time.monotonic_ns()
t1 = time.monotonic_ns()
print(f'{(t1-t0)/n:.1f} ns per call')
"
Where this leads next
This chapter named the monotonic clock as the kernel's promise of single-machine non-decreasing time. It is the foundation for every interval measurement, timeout, and deadline in code that needs to survive NTP corrections. What it does not solve is cross-machine ordering — and the next chapters in Part 3 are about exactly that.
- Clock skew between machines — what the actual cross-machine skew distribution looks like inside an availability zone, across regions, and over the public internet, and why "synchronised within X ms" is a probability statement not a guarantee.
- Logical clocks (Lamport) — Leslie Lamport's 1978 answer to "happened-before" without ever measuring physical time; the foundational construction every causality-respecting protocol descends from.
- Vector clocks — the strict generalisation that detects concurrency, used in Dynamo and Riak.
- Hybrid logical clocks (HLC) — the practical synthesis used by CockroachDB, MongoDB, YugabyteDB, where physical and logical time are fused into a single 64-bit value.
- TrueTime and Spanner's bounded uncertainty — what the monotonic-clock contract looks like extended to a global wall clock, with hardware infrastructure paying for the bound.
The unifying pattern: monotonic time is the single-host primitive on which all higher-level distributed-time abstractions are built. Lamport timestamps don't directly use it, but every implementation that measures "wait at least X milliseconds before re-trying" uses it underneath. HLC explicitly composes a monotonic-clock reading with a logical counter. TrueTime's uncertainty bound is computed in monotonic-time units. If your code holds a monotonic-time reading across machines (sending it in a network message and comparing on the receiver), you have introduced a bug — the next chapter explains why.
References
- Lamport, "Time, Clocks, and the Ordering of Events in a Distributed System" — CACM 1978. The foundational paper that motivated the wall-clock vs monotonic-vs-logical distinction; required reading for anyone working on distributed clocks.
- POSIX
clock_gettime(2)man page — the binding specification of CLOCK_MONOTONIC, CLOCK_MONOTONIC_RAW, CLOCK_BOOTTIME, and the suspend-behaviour distinctions. - Linux Kernel
kernel/time/timekeeping.c— the implementation, includingtimekeeping_update_from_shadowwhich enforces the monotonicity guarantee. - Davidlohr Bueso, "vDSO and the speed of clock_gettime" — LWN article on how the vDSO makes
clock_gettimecheap. - Cloudflare, "How and why the leap second affected Cloudflare DNS" — production post-mortem in which Cloudflare's resolver hit a non-monotonic wall-clock interaction; the chapter's KapitalKite story is a fictionalised cousin.
- Java
System.nanoTime()documentation — the cleanest public documentation of the wall-vs-monotonic semantic distinction in any standard library. - Wall clocks and NTP — internal cross-link to the previous chapter; this chapter completes the "two clocks, two jobs" pair.
- Designing Data-Intensive Applications, Chapter 8 — Kleppmann, O'Reilly 2017. The "Unreliable Clocks" section's treatment of monotonic clocks as the safe alternative for intervals.