Heads up: posts on this site are drafted by Claude and fact-checked by Codex. Both can still get things wrong — read with care and verify anything load-bearing before relying on it.
why → how

Why monotonic time is different from wall-clock time

Wall-clock time tells you what to put on a calendar. Monotonic time tells you how long something took. Confusing them is how you get bugs that look like physics violations.

Systems intermediate Apr 29, 2026 · updated Aug 25, 2026 · 11 min read

On this page

The picture version

The whole idea in six pictures, for a reader who has never wondered where a computer’s clock comes from. The prose below fills in the seams.

1 · The problem

A request that finished before it started.

0 −23 minutes start = time.time() elapsed = time.time() - start produced this
The most ordinary code in the world, on the most ordinary day. Nothing was slow — the graph is claiming a request finished before it began.

2 · What actually happened

The clock moved between your two readings.

real time, moving forward you read the clock 10:00:30 you read it again 10:00:07 NTP: “you are 23 minutes fast” so the clock steps backwards 10:00:07 − 10:00:30 = −23 min
Both readings were honest. The clock was corrected in between, and a subtraction has no way to find that out.

3 · Why no clock can fix it

Two questions. One clock can only answer one.

WALL CLOCK “what time is it?” must agree with calendars, time zones, the whole world so: correctable MONOTONIC CLOCK “how long has it been?” so: nobody may touch it
A clock that must keep agreeing with the world has to be correctable — and correcting sometimes means going backwards. A clock that must never go backwards therefore cannot be corrected. These are not the same clock.

4 · The fix

One counter, two different things done to it.

ONE COUNTER ticking on the CPU + civil-time offset + nothing at all WALL CLOCK time.time() NTP may step it backwards to keep it honest unsafe to subtract MONOTONIC time.monotonic() no steps, no leap seconds, no idea what day it is safe to subtract
Both clocks usually read the same hardware counter. The whole difference is what the kernel does with it before handing you a number — and swapping time.time() for time.monotonic() makes the negative bar impossible.

5 · What it costs you

A clock nobody may correct can't be compared with anyone else's.

machine A monotonic reads 412,388 seconds since its own boot machine B monotonic reads 9,004 seconds since its own boot − = a meaningless number that will look perfectly plausible
Each machine counts from its own arbitrary zero, usually its boot. Cross-machine timing has to fall back on wall-clock timestamps, or give up on durations and order events causally instead.

6 · Keep this card

The whole thing on one index card.

TWO CLOCKS = a calendar you may correct + a stopwatch nobody may touch + a zero point that means nothing …anywhere but on this one machine
Picture to keep: a wall calendar and a stopwatch bolted to the same wall. Anyone may walk up and correct the calendar. Nobody may touch the stopwatch — and it has no idea what day it is.

Why it exists

Someone opens your latency dashboard and there’s a bar hanging below zero. A request took negative twenty-three minutes. Nobody deployed anything. The service was healthy. The graph is just claiming that a thing finished before it started.

The code that produced it is the most natural code in the world:

start = time.time()
handle_request()
elapsed = time.time() - start

We’ll follow that one measurement the whole way down. Most days it works. That night it didn’t, because the machine’s clock got adjusted underneath it — by NTP nudging the wall clock backwards to correct drift, by a virtual machine pausing and resuming, by an admin running date -s to fix a wrong clock, or by a leap second being absorbed.

You probably assume the fix is a better clock — tighter NTP, a nicer time library, more decimal places. It isn’t, and no amount of clock accuracy would have saved that measurement. The problem is that two different questions were asked of one clock, and they need two different clocks:

  1. What time is it right now, in the human world? That’s wall-clock time: it must agree with calendars, time zones, NTP, and ultimately the rotation of the Earth. To stay agreeing, it has to be correctable — and corrections sometimes mean moving it backwards.
  2. How long has it been since X? That’s monotonic time: it must never go backwards and never jump. It doesn’t even need to be a meaningful date — it just needs to count forward from some arbitrary fixed point.

Modern operating systems expose both because trying to make one clock do both jobs is a contradiction. You can’t simultaneously promise “this matches civil time” and “this never goes backwards” — civil time itself goes backwards sometimes.

Why it matters now

The specific thing that has changed is where code runs. Ephemeral cloud instances and GPU boxes spun up for a single job come up with a clock that hasn’t been disciplined yet, and get yanked into shape by NTP shortly after. That’s a window, on every freshly booted machine, in which any time.time() subtraction can produce the negative bar above — and short-lived workloads spend a disproportionate share of their lives inside it. (Containers don’t add a second window: a pod reads the host’s CLOCK_REALTIME, so it inherits whatever correction the host is in the middle of.)

It matters for everything where “elapsed time” is load-bearing:

The bugs are nasty because they’re rare, system-dependent, and they look like the laws of physics broke.

The short answer

monotonic time = "seconds since some fixed point" + "never goes backwards, never jumps"

Picture to keep: a wall calendar and a stopwatch, bolted to the same wall. Someone is allowed to walk up and correct the calendar whenever it disagrees with the rest of the world. Nobody is allowed to touch the stopwatch while it’s running — and it doesn’t know what day it is.

Wall-clock time answers “what time is it?”. Monotonic time answers “how much time has passed?”. They are different problems, so the OS gives you different clocks. Use wall-clock for things humans see (logs, scheduled jobs, “created_at”). Use monotonic for every duration, deadline, and timeout your code computes.

How it works

Attempt 1: one clock, kept correct

This is what produced the negative bar. There is one clock; a daemon keeps it agreeing with the outside world; you subtract two readings to get a duration.

Why it breaks: “kept correct” and “safe to subtract” are incompatible requirements. Correcting a clock that’s ahead means moving it backwards, and the moment it moves backwards between your two readings, your duration is wrong — sometimes negative, sometimes hugely positive if it was moved forwards. The clock did its job. Your subtraction was never entitled to assume otherwise.

Attempt 2: just never adjust it, then

Freeze the clock’s rate and let it free-run. Now subtraction is safe.

Why it breaks: every crystal drifts, so within days the machine disagrees with the rest of the world about what time it is, and every timestamp in your logs, every created_at, every cron schedule is wrong. You fixed durations by breaking calendars.

Attempt 3: two clocks from one counter

Neither requirement can be dropped, so the OS stops trying to satisfy both with one number. Both clocks are usually derived from the same hardware source — on modern x86, typically the TSC. (Reads of either clock usually go through the vDSO, which is how they’re fast — but that’s the access path, not the clock.) The difference between the two clocks is entirely what the OS does with that counter before handing you a number.

For wall-clock time, the kernel keeps an offset from the counter to civil time, and ntpd/chrony/systemd-timesyncd continuously adjust that offset. There are two adjustment modes:

For monotonic time, the kernel drops the civil-time offset entirely: no steps, no date -s, no leap seconds. (It does not drop everything — CLOCK_MONOTONIC is still frequency-adjusted by NTP, which is the distinction the next section turns on.) On Linux that’s clock_gettime(CLOCK_MONOTONIC, …). Swap time.time() for time.monotonic() in the snippet above and the negative bar becomes impossible.

Attempt 4: pick which monotonic you meant

One monotonic clock isn’t quite enough either, because “never goes backwards” leaves two questions open — should it be slewed with NTP, and should it tick while the machine is asleep? Linux answers by shipping several flavours:

In application languages:

The pattern across all of them is the same: there are two functions because there are two questions.

Show the seams

A few things the textbook account skips:

You started with monotonic time = "seconds since some fixed point" + "never goes backwards, never jumps". What did the negative-latency bar add to that line? — + the fixed point is arbitrary and local, which is the price of the guarantee. A clock that refuses to be corrected can’t be compared with anyone else’s, which is why monotonic time is useless for logs and mandatory for durations. “What time is it” and “how long did this take” are not the same question, and the universe doesn’t owe us one clock that answers both.

Check yourself

Before you go — you fix the dashboard by switching to time.monotonic(), and the negative bars stop. Then someone asks you to correlate that latency spike with a spike on a different machine. Can you subtract one box’s monotonic timestamp from the other’s?

Answer

No. Each machine’s monotonic clock counts from an arbitrary, unrelated zero — often boot — so the difference between two of them is a meaningless number that will nonetheless look plausible. Cross-machine correlation has to use wall-clock timestamps (accepting their sync error, typically milliseconds under decent NTP) or a logical clock that orders events by causality instead of by time. The rule of thumb: monotonic for durations within one machine, wall-clock for anything two machines have to agree on.

And a diagnosis: a laptop app measures a 30-second timeout with CLOCK_MONOTONIC. The user closes the lid, goes to lunch, and reopens it an hour later. Has the timeout fired?

Answer

On Linux, no — CLOCK_MONOTONIC does not advance while the system is suspended, so from the app’s perspective almost no time passed. That is exactly why CLOCK_BOOTTIME exists: same never-backwards guarantee, but it includes suspended time. Which one your language’s monotonic() maps to is a runtime decision and isn’t always documented — worth checking before you rely on a long timeout surviving a sleep.

Going deeper