A duration is elapsed = end - start and must be ≥ 0. Read start and end from a time-of-day clock and let NTP step it back 5 s during a 2 s measurement, and elapsed comes out −3.000 s — a lease check elapsed > 10 then reads False forever and the lease never expires. Time the same interval with a monotonic clock, which has no external authority for NTP to correct, and the step is invisible: elapsed stays the true +2.000 s.
A duration is the simplest measurement in the world: elapsed = end - start, and it must be >= 0. Read start and end from a time-of-day clock (System.currentTimeMillis(), time.time()) and the number is only meaningful if nothing moved the clock in between. But a time-of-day clock is a synchronized value — NTP can step it backward at any moment to correct drift. Step it backward by 5 s during a 2 s measurement and end - start comes out −3.000 s: a negative duration. A lease/timeout check built on it — if elapsed > 10: release — then reads −3 > 10 as False and the lease looks un-expired forever. Measure the same interval with a monotonic clock — a local counter that only ever counts up, with no external authority for NTP to correct — and the step is invisible: elapsed stays the true +2.000 s. This is the book's core reason there are two clocks.
Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 9 — The Trouble with Distributed Systems, §"Monotonic Versus Time-of-Day Clocks" and §"Relying on Synchronized Clocks":
A monotonic clock is suitable for measuring a duration (time interval), such as a timeout or a service's response time [...] The name comes from the fact that they are guaranteed to always move forward (whereas a time-of-day clock may jump back in time).
The book's rule is blunt: use a time-of-day clock to know what time it is, and a monotonic clock to know how long something took. Here you watch what happens when you break that rule — you compute a duration from the time-of-day clock and an NTP step corrupts it.
The surprise is that subtraction — end - start — can produce a negative duration. These place why; one line each, with a free resource, and the one that carries the result flagged.
measure_* functions computing the same end - start with opposite guarantees.clock.step(-5.0).time module docs on time.monotonic() vs time.time() — the docs state plainly that monotonic "cannot go backward." NTP slewing vs stepping and leap seconds (#2, #3): the ntpd documentation on the step/slew threshold; Google's "leap smear" write-up for how a leap second is spread out to avoid a backward step. If only one is new, make it #1 — every other line is a consequence of the two clocks answering two different questions.
918273.000) is an arbitrary tick count (uptime-since-boot), deliberately unrelated to wall time — you cannot read civil time off it. That is the point: a monotonic clock has no meaningful absolute value, only a reliable difference, which is the exact +2.000 s interval.
Two clocks answer two different questions. Use each for its job.
Both let you write elapsed = end - start. Only one keeps that subtraction honest when the clock is corrected. The whole thing is one script, code/monotonic_vs_wallclock.py.
cd study/ddia/ch09/code
python3 monotonic_vs_wallclock.py
Do not read the output yet — make each prediction first.
Predict. We read start from a time-of-day clock, 2 s of work passes, then NTP decides the clock is 5 s fast and steps it back 5 s, then we read end. What does elapsed = end - start come out to?
−3.000 s — a negative duration. The clock ran forward 2 s (to …402) then NTP yanked it back 5 s (to …397), so end is earlier than start. Nothing errored; the subtraction just produced something no stopwatch could.
Predict. The identical modeled interval — 2 s of work, then the same NTP step — but timed by a monotonic clock. The step arrives, but a monotonic clock has no external authority to jump it. What does elapsed come out to?
+2.000 s — the true interval. The absolute value 918273.000 is an arbitrary tick count (uptime), deliberately not a wall-clock time — you can't read civil time off it. But the clock only counts up, and the NTP step is invisible to it (step() is a no-op), so end − start recovers exactly the 2 s of work that actually elapsed. This is the same interval Step 1 measured; only the clock differs.
Predict. A lease says "release me once more than 10 s have elapsed": if elapsed > 10: release. Run it on the wall-clock −3.000 s and on the monotonic +2.000 s. Which lease looks expired?
Both read False — but for opposite reasons. The monotonic check is correctly False: exactly 2 s passed, the lease genuinely hasn't hit 10 s, so holding is right. The wall-clock check is silently broken: −3 > 10 is False too, but it would stay False for any lease window — a negative duration can never exceed a positive threshold. The holder waits for an expiry that arithmetic guarantees will never come. (Had the step gone the other way, elapsed would balloon and the lease would expire instantly instead.)
918273.000) is an arbitrary uptime tick, but the difference is exact.elapsed > lease is unreachable once elapsed is negative, so the lease never expires.From first principles the two clocks are different kinds of quantity. A time-of-day clock is a synchronized value: it is supposed to agree with an external authority (UTC, delivered by NTP), and the price of that agreement is that the authority may correct it in either direction at any time — forward when the clock is slow, back when it is fast, and abruptly (a step) when the error is too large to slew away gently. So two readings taken across a correction are drawn from different calibrations of the same dial; subtracting them is meaningless, and when the correction is backward and larger than the real elapsed time, the difference goes negative. That is exactly what Step 1 shows: +2 s of real work minus a −5 s step leaves −3 s.
A monotonic clock is the opposite: a local counter with no external authority and no obligation to match anyone. Nothing is allowed to reset it, so by construction each reading is >= the previous one and every difference is a valid, non-negative duration. Its absolute value is worthless — you can't tell what time it is from 918273.000 — but duration was the only thing you wanted, and duration is precisely what it protects. The NTP step lands on the wall clock and passes straight through the monotonic one, which is why the same interval reads −3.000 s on one and +2.000 s on the other.
The bug in Step 3, then, is a category error: measuring a duration (how long) with a clock built to report an instant (what time). elapsed = end - start conflates the two — it looks like a duration but inherits the wall clock's power to jump. The fix is not more careful NTP or a sanity check for negatives; it is to read durations from the clock that answers the duration question. Java's System.nanoTime(), POSIX CLOCK_MONOTONIC, Python's time.monotonic() all exist for this one reason.
date set will all step the clock regardless. You cannot rely on never being stepped, so the rule is unconditional: measure durations, timeouts, and leases with a monotonic clock.
CLOCK_MONOTONIC vs CLOCK_BOOTTIME. On Linux, CLOCK_MONOTONIC pauses while the machine is suspended, so a lease measured across a laptop sleep can under-count; CLOCK_BOOTTIME keeps counting. Decide which your timeout actually wants.+5 s (NTP finds the clock slow) and watch the wall-clock elapsed jump to +7 s; wire it to a shorter lease and see the opposite failure — a healthy holder evicted early.end - start — a micro-benchmark, a request timer, a heartbeat gap — is a duration and inherits this bug. Audit one place in your own code that times something with time.time().Sources: DDIA 2e, Ch. 9, §"Monotonic Versus Time-of-Day Clocks", §"Relying on Synchronized Clocks" · Python time module documentation.