skip to content

Why measure elapsed time with time.monotonic() instead of time.time() in Python?

level: juniorimportance: must knowfreq 60%

answer

  1. Two clocks answer two different questions
  2. One of them can jump backwards
  3. NTP steps, snapshots and manual date changes
  4. Durations versus naming an instant
  5. monotonic() has no epoch at all

basics

~20 s

time.monotonic() only ever moves forward, so the difference between two readings is a true duration. time.time() names a wall-clock instant, and NTP or an operator can step it forwards or backwards, corrupting any elapsed-time calculation.

solid answer

~50 s

Python's `time` module exposes two different clocks because they answer two different questions. `time.time()` answers *what instant is it* — seconds since the Unix epoch, which you can turn into a calendar date, write into a log or compare with a timestamp from another host. `time.monotonic()` answers *how much time has passed* — seconds counted from an unspecified reference point that never decreases. The wall clock is settable: an NTP correction can step it, a virtual machine resumed from a snapshot inherits a jump, and an operator can set it by hand, so `end - start` over `time.time()` can come out negative or hours long. `time.monotonic()` cannot jump, so it is the right basis for timeouts, retry backoff and rate limits. `time.perf_counter()` is also monotonic and offers the finest resolution the platform has, so it is the one to reach for on short measurements.

code

python · 8 lines
python
import time

start = time.monotonic()
time.sleep(0.05)
print(f"elapsed {time.monotonic() - start:.3f}s")

print(time.get_clock_info("monotonic").adjustable)  # False
print(time.get_clock_info("time").adjustable)       # True

go deeper

for a junior

Recall the one-line rule: durations come from time.monotonic(), an instant comes from time.time(). Be able to name at least one reason the wall clock can move, such as an NTP correction.

for a middle

Explain the mechanics: what settable means, that time.get_clock_info() reports adjustable, and which everyday operations belong on which clock — timeouts and backoff on monotonic, log timestamps and stored expiry on the wall clock.

for a senior

Show you have debugged this. Be ready to describe a negative duration or a timeout that fired an hour late, and to say what monotonic still does not guarantee: an undefined epoch, rate slewing, and platform-dependent behaviour across suspend.

for a principal

Own it as a convention rather than a habit. Decide where in a codebase clock reads are allowed at all, how time is injected so it can be faked in tests, and what the standard is for durations recorded in metrics versus timestamps recorded in logs.

## Two clocks because there are two questions Operating systems keep more than one notion of time, and Python surfaces them as separate functions in the `time` module rather than trying to paper over the difference. **The wall clock** — `time.time()`, and its integer sibling `time.time_ns()` — returns seconds since the Unix epoch (1970-01-01 UTC). Its value *means* something on its own: it names an instant, it can be rendered as a calendar date, and two machines that both read it are talking about the same moment. That is exactly what a log record, a token expiry or a row's `created_at` needs. **The monotonic clock** — `time.monotonic()`, `time.monotonic_ns()` and `time.perf_counter()` — returns seconds counted from a reference point the platform does not define. Its absolute value means nothing. The only meaningful operation is subtracting two readings taken in the same process on the same machine, and that difference is a real elapsed duration. ## Why the wall clock lies about durations The system clock is *settable*, and several ordinary things set it: * **NTP steps.** A time daemon that finds a large offset does not gently correct it; past a threshold it calls the "set the clock now" syscall and the clock jumps, possibly backwards. * **Boot.** A machine with a bad or missing hardware clock starts near the epoch and is corrected seconds later, which is a huge forward jump right when your service is starting up. * **Virtualization.** A guest resumed from a snapshot or migrated between hosts sees its clock corrected on the way in. * **Humans and containers.** Someone sets the date by hand, or a container inherits a host adjustment. Note what is *not* on that list: daylight saving. `time.time()` is UTC-based, so a DST transition never moves it — that is a separate concern belonging to the timezone-aware side of `datetime`. The failure modes are all quiet. A duration comes out negative and gets logged as a nonsense metric. A timeout computed as `time.time() + 30` fires instantly, or an hour late. A rate limiter that stores wall-clock timestamps opens the floodgates for an hour after the clock steps backwards. In one common shape — a batch tool that times each of, say, a 340-case regression pack with `time.time()` differences — a single NTP step mid-run poisons one case's timing, and because the totals are floats the bad value quietly drags the aggregate report off without raising anything. ## What monotonic guarantees, and what it does not Guaranteed: the value never decreases, and it is not affected by the clock being *set*. `time.get_clock_info("monotonic").adjustable` is `False`, while `time.get_clock_info("time").adjustable` is `True` — the standard library will tell you which is which at runtime. Not guaranteed: * **A stable rate.** A time daemon that corrects a small offset by *slewing* changes the clock's rate rather than stepping it, and on Linux the monotonic clock is slewed too. It never jumps, but it may run a fraction of a percent fast or slow while a correction is being absorbed. That is irrelevant for a timeout and relevant for a precision measurement. * **A meaningful epoch.** The reference point differs between platforms and between boots, so a monotonic reading must never be persisted, logged as a timestamp, or shipped to another service. * **Counting through suspend.** Whether the monotonic clock advances while the machine is asleep is platform-dependent. A laptop closed for an hour can wake with a "5 second" timeout that has not expired. ## Where perf_counter fits `time.perf_counter()` is documented as the clock with the highest available resolution for measuring a short interval, and it includes time spent asleep. On several platforms it and `time.monotonic()` read the same underlying counter; the distinction is one of contract, not always of mechanism. Use `perf_counter` when you are measuring something small, `monotonic` when you are enforcing a deadline. If you want CPU time rather than elapsed time — excluding sleeping and blocking — that is `time.process_time()` or `time.thread_time()`, which is a different question again. ## The rule to state in an interview Durations, timeouts, retry backoff, rate limiting, in-process cache TTLs: monotonic. Anything that names an instant and leaves the process — log timestamps, expiry stored in a database, a value compared with another machine's clock: the wall clock, stored as UTC. Mixing them is the bug; picking per question is the fix.

  • What does the absolute number returned by time.monotonic() actually represent?
    Nothing you can rely on. The reference point is deliberately unspecified and differs between platforms and between boots, so only the difference of two readings taken in the same process is meaningful. Never persist a monotonic reading, never log it as a timestamp, and never send it to another service expecting it to be comparable there.
  • Does time.monotonic() keep advancing while the machine is suspended?
    Not guaranteed, and it varies by platform. On Linux CPython reads the kernel's monotonic clock, which does not advance across a suspend, so a laptop closed for an hour can wake with a monotonic-based deadline that has barely moved. If suspended time must count toward a deadline, you need a wall-clock cross-check alongside the monotonic one.
  • Is time.monotonic() completely immune to NTP?
    It is immune to clock *steps* — it never jumps and never goes backwards. It is not immune to *slewing*: when a time daemon corrects a small offset by nudging the clock's rate, the monotonic clock's rate is nudged too on platforms like Linux. That is harmless for timeouts and worth knowing for sub-microsecond measurement.

A wall clock tells you what time the meeting starts; a stopwatch tells you how long the meeting ran. Resetting the wall clock mid-meeting does not change the stopwatch.

saying these in an interview costs you the question

  • Claims the system wall clock never moves backwards
  • Uses time.time() differences to enforce a timeout
  • Thinks time.monotonic() returns seconds since the Unix epoch
  • Believes NTP only ever slews and never steps the clock
  • Logs or persists a monotonic reading as a timestamp
  • Says a DST transition changes the value of time.time()

context