Skip to content

Web Development · Backend Engineering

Wall Clock vs Monotonic Clock: Measure Time Right

Use a monotonic clock for durations and timeouts, the wall clock only for timestamps. Python, JavaScript and Go code, plus the leap-second bug that shows why.

Abhishek Gupta

Abhishek Gupta

5 min read

Two clay pocket watches side by side, one tangled in loose thread and the other calm with a smooth ribbon beneath it

Sponsored

Share

Use a monotonic clock to measure how long something took, and the wall clock only to say when something happened. If you subtract two Date.now() or time.Now() readings to get a duration, you have a bug that is simply waiting for the clock to move.

It sounds pedantic until you read what happened to Cloudflare’s DNS on New Year’s Day 2017.

The bug: a negative duration

At midnight UTC on January 1, 2017, a leap second was applied. Cloudflare’s RRDNS service, written in Go, measured how fast upstream resolvers answered by subtracting two time.Now() values:

rtt := time.Now().Sub(start)

Cloudflare’s post-mortem puts it plainly: rtt could be negative if time.Now() was earlier than start. Their code then smoothed many such measurements, and eventually the smoothed value went negative. That number was fed into weighted server selection, which called rand.Int63n(), and rand.Int63n panics when its argument is negative.

The blast radius was small, which is the only reason it’s a story we can laugh at. About 0.2% of DNS queries and under 1% of HTTP requests saw errors at the peak, and the impact ended at 06:45 UTC. The cause, in Cloudflare’s own words, was the belief that time cannot go backwards.

Two clocks, two jobs

Your machine keeps two different notions of time, and they exist because the jobs conflict.

Wall clockMonotonic clock
Answers”What time is it?""How much time has passed?”
Can jump backwardYes (NTP step, admin change, leap second)No
Meaningful on its ownYes, it’s a calendar timeNo, the starting point is arbitrary
Use forLog timestamps, created_at, certificates, schedulesTimeouts, latency, retries, rate limits, TTLs in memory

Python’s documentation describes time.monotonic() as a clock “that cannot go backwards” and “not affected by system clock updates,” with an undefined reference point so that only the difference between two calls is valid. The same docs warn that time.time() “can return a lower value than a previous call if the system clock has been set back between the two calls.”

That one sentence is the entire argument.

The fix, language by language

Python

import time

start = time.perf_counter()
result = do_work()
elapsed = time.perf_counter() - start     # seconds, never negative

time.monotonic() works the same way for timeouts and deadlines. perf_counter() is the one for short measurements, because it has the highest available resolution. On CPython, both use the same monotonic clock.

A deadline loop is where this matters most:

deadline = time.monotonic() + 30
while time.monotonic() < deadline:
    if try_connect():
        break
    time.sleep(0.5)

Write that with time.time() and a clock step of an hour turns a 30-second retry into either an instant failure or a wait of an hour.

JavaScript and Node

const t0 = performance.now();
await doWork();
const ms = performance.now() - t0;        // milliseconds, fractional

performance.now() exists in browsers and Node. For nanosecond-scale work in Node, process.hrtime.bigint() returns a BigInt you subtract the same way. Keep Date.now() for timestamps you store or display.

Go

Modern Go reads both clocks when you call time.Now() and uses the monotonic reading automatically when you subtract:

start := time.Now()
doWork()
elapsed := time.Since(start) // uses the monotonic reading

The trap is that the monotonic reading is stripped when you do things like Round(0), convert to Unix time, or serialise and deserialise the value. Compare a Time that crossed a process boundary and you’re back on wall-clock subtraction.

Rust and others

std::time::Instant is monotonic and SystemTime is wall clock. The names are the pattern: nearly every language gives you both, and the bug is almost always reaching for the familiar one.

What to store, what to compare

A rule that prevents most of these bugs:

  1. Store wall-clock time (as UTC) in databases, logs and tokens. It’s the only kind that means anything to another machine.
  2. Never persist a monotonic reading and compare it after a restart. The starting point is arbitrary and different on every boot.
  3. Convert at the edge. Compute expires_at = now_wall + ttl when you create a token. Inside one process, enforce the TTL with the monotonic clock.
  4. Don’t assume two machines agree. Even with a perfect single-machine clock, ordering events across servers needs another tool entirely, such as vector clocks.

Are leap seconds going away?

Yes, eventually. Resolution 4 of the 27th CGPM in 2022 decided that the maximum permitted difference between UT1 and UTC will be increased in or before 2035, and it asks for a plan so that UTC stays continuous for at least a century. The resolution leaves the new maximum value to a later proposal.

Good news, but don’t relax. Leap seconds were never the only reason a wall clock moves. An NTP daemon can step a drifting clock, a virtual machine can resume from a pause with stale time, and an administrator can type date -s. A bug that only fires every few years is still worth removing, and a bug that fires when someone resumes a laptop lid will not wait for 2035.

The takeaway

Search your codebase for subtraction between two wall-clock reads: Date.now() - , time.time() - , time.Now().Sub. Each hit that measures a duration, timeout or rate should move to a monotonic clock. It’s a small diff, and it’s a bug class you can remove completely. If your team wants a review of reliability basics like this across a production codebase, the services page is the place to start.

Frequently asked questions

What is the difference between a wall clock and a monotonic clock?
A wall clock tells you the date and time and can be adjusted, so it can jump backward or forward. A monotonic clock only moves forward at a steady rate and has an arbitrary starting point, so only the difference between two readings means anything. Use the first for timestamps and the second for elapsed time.
Should I use time.time() or time.monotonic() in Python?
Use time.monotonic() or time.perf_counter() to measure durations. Python's documentation says monotonic() cannot go backwards and is not affected by system clock updates, while time.time() can return a lower value than a previous call if the system clock has been set back. Use time.time() when you need a calendar timestamp.
Is Date.now() safe for measuring how long something took?
It works until the system clock changes, then your measurement can be wrong or negative. performance.now() is the better choice for elapsed time in browsers and Node because it is intended for high-resolution durations and does not depend on the calendar. Keep Date.now() for timestamps.
Do leap seconds still matter if they are being abolished?
For now, yes. The 27th CGPM decided in 2022 that the maximum difference between UT1 and UTC will be increased in or before 2035, which ends leap seconds in practice, but none of that changes how your servers behave today. NTP step corrections and manual clock changes can also move the wall clock backward.

Sources

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored