Monotonic Realtime and Boottime Clocks

Linux exposes several named POSIX clocks through one clock_gettime() interface, and they differ in ways that matter enormously for correctness. CLOCK_MONOTONIC never goes backward, starts at roughly system boot, and is rate-adjusted by the Network Time Protocol (NTP) but never stepped. CLOCK_REALTIME is wall-clock date-and-time in Coordinated Universal Time (UTC), is NTP-steerable, and can jump forward or backward when an administrator or daemon sets the time. CLOCK_BOOTTIME is monotonic plus the time the system spent suspended. CLOCK_MONOTONIC_RAW tracks the bare hardware rate with no NTP adjustment at all. CLOCK_TAI is atomic time (International Atomic Time) — monotonic and continuous, with no leap-second discontinuities. Internally these are not five independent counters; they are one monotonic base plus three precomputed offsets (offs_real, offs_boot, offs_tai) maintained by the timekeeping core (include/linux/timekeeper_internal.h v6.12). The single most important practical rule follows directly: measure durations with CLOCK_MONOTONIC, never CLOCK_REALTIME, because realtime can be stepped out from under you.

This note pins to the 6.12 long-term-support (LTS) kernel (released 2024-11-17). Parent map: Linux Time and Timers MOC. The mechanism that produces these clocks is The Timekeeping Core; the language-level echo of the same distinction is time.Time Comparison and the Monotonic Clock.


Mental Model

Picture one master timeline — a monotonic count of nanoseconds since boot that only ever moves forward — and three offsets layered on top of it, each shifting that timeline to answer a different question. To get wall-clock time, add the realtime offset; to include suspend, add the boot offset; to get atomic time, add the TAI offset. The named clocks are not separate hardware; they are the same monotonic base read through different lenses. This is exactly how the kernel implements them: ktime_get_with_offset(offs) adds the live monotonic time to one of three offsets, offs_real, offs_boot, or offs_tai (kernel/time/timekeeping.c:881-900 v6.12).

flowchart TB
  MONO["CLOCK_MONOTONIC<br/>nanoseconds since ~boot<br/>(never steps; NTP rate-slewed)"]
  RAW["CLOCK_MONOTONIC_RAW<br/>bare hardware rate<br/>(no NTP at all)"]
  MONO -->|"+ offs_real"| REAL["CLOCK_REALTIME<br/>UTC wall-clock date<br/>(steppable, NTP-steered)"]
  MONO -->|"+ offs_boot"| BOOT["CLOCK_BOOTTIME<br/>monotonic + suspend time"]
  MONO -->|"+ offs_tai"| TAI["CLOCK_TAI<br/>atomic time, no leap seconds"]
  RAW -. "shares the same counter,<br/>but tkr_raw keeps the<br/>original mult" .-> MONO

The named clocks as offsets over one monotonic base. What it shows: CLOCK_MONOTONIC is the spine; realtime, boottime, and TAI are each just that spine plus a precomputed offset, while CLOCK_MONOTONIC_RAW is a parallel reading of the same hardware counter that deliberately skips NTP. The insight to take: because realtime is “monotonic + offs_real”, stepping the wall clock is implemented by changing offs_real (and xtime_sec) while leaving the monotonic spine untouched — which is precisely why durations measured on the spine survive a wall-clock jump, and durations measured on realtime do not.


The Clock IDs

The clock identifiers are stable user-space ABI, defined in include/uapi/linux/time.h v6.12:

#define CLOCK_REALTIME			0
#define CLOCK_MONOTONIC			1
#define CLOCK_PROCESS_CPUTIME_ID	2
#define CLOCK_THREAD_CPUTIME_ID		3
#define CLOCK_MONOTONIC_RAW		4
#define CLOCK_REALTIME_COARSE		5
#define CLOCK_MONOTONIC_COARSE		6
#define CLOCK_BOOTTIME			7
#define CLOCK_REALTIME_ALARM		8
#define CLOCK_BOOTTIME_ALARM		9
#define CLOCK_TAI			11

These numeric IDs are passed to clock_gettime(clockid_t, struct timespec *). Their wall/monotonic semantics are documented in clock_gettime(2). The five this note covers are REALTIME, MONOTONIC, MONOTONIC_RAW, BOOTTIME, and TAI; the *_COARSE variants are the same clocks read cheaply as of the last tick, and the *_ALARM variants are boottime/realtime clocks that can wake a suspended system (covered in the timer-interface notes). CLOCK_TAI is ID 11, not 10 — ID 10 is the retired CLOCK_SGI_CYCLE, kept only as a placeholder, as the header comments note (“Do not reuse!”).


CLOCK_MONOTONIC — The Spine

CLOCK_MONOTONIC is “a nonsettable system-wide clock that represents monotonic time since — as described by POSIX — some unspecified point in the past” (clock_gettime(2)). On Linux that point is roughly system boot. Three properties define it:

  1. It never goes backward. No system call can step it; reads are guaranteed non-decreasing (modulo the few-nanosecond fast-path jumps allowed only on the NMI-safe *_fast_ns accessors).
  2. It is rate-slewed by NTP. The man page is explicit that CLOCK_MONOTONIC is “affected by the incremental adjustments performed by adjtime(3) and NTP” (clock_gettime(2)). This surprises people: monotonic is not the raw hardware rate. It shares the NTP-disciplined mult from the timekeeping core’s tkr_mono, so when NTP slews the system frequency to track true time, monotonic speeds up or slows down with it. What it never does is jump.
  3. It excludes suspend. Per the kernel docs, ktime_get() (the kernel-internal reader for CLOCK_MONOTONIC) “starts at system boot time but stops during suspend” (kernel docs). Time spent in S3/suspend-to-RAM does not appear in monotonic.

Because it is monotonic and immune to wall-clock steps, this is the clock for measuring elapsed time: timeouts, latency measurement, rate limiting, retry backoff, benchmarks. The kernel reads it via ktime_get() (see The Timekeeping Core).


CLOCK_REALTIME — Wall-Clock, and It Can Jump

CLOCK_REALTIME is “a settable system-wide clock that measures real (i.e., wall-clock) time” in seconds and nanoseconds since the UNIX epoch (1970-01-01 UTC) (clock_gettime(2)). It is the clock you format into a date, write into file timestamps, and stamp on log lines. Two properties make it dangerous for duration math:

  1. It is NTP-steerable. The man page says it “is affected by discontinuous jumps in the system time (e.g., if the system administrator manually changes the clock), and by frequency adjustments performed by NTP” (clock_gettime(2)).
  2. It can be stepped. A privileged process calling clock_settime(CLOCK_REALTIME, ...) or settimeofday() makes realtime jump — forward if the clock was behind, backward if it was ahead. NTP daemons (ntpd, chrony) normally prefer to slew small corrections, but they step when the offset is large (typically at startup or after a long outage).

Here is the mechanism that makes monotonic safe and realtime unsafe, from the same code path. do_settimeofday64() (timekeeping.c:1466-1474 v6.12) does exactly two relevant things under the timekeeper lock:

tk_set_wall_to_mono(tk, timespec64_sub(tk->wall_to_monotonic, ts_delta));
tk_set_xtime(tk, ts);

tk_set_xtime jumps the wall-clock seconds to the new value — CLOCK_REALTIME steps. But tk_set_wall_to_mono simultaneously moves wall_to_monotonic by the opposite delta, so the monotonic timeline (xtime + wall_to_monotonic) is left exactly where it was. The realtime offset (offs_real) is recomputed from the new wall_to_monotonic, and offs_tai follows. The result: setting the wall clock changes realtime and TAI, but CLOCK_MONOTONIC does not move at all. This is the kernel-level proof of the durations rule below.


CLOCK_BOOTTIME — Monotonic Plus Suspend

CLOCK_BOOTTIME was added in Linux 2.6.39 (clock_gettime(2)). It is “identical to CLOCK_MONOTONIC, except it also includes any time that the system is suspended.” The kernel docs describe its reader ktime_get_boottime() as “Like ktime_get(), but does not stop when suspended” (kernel docs).

The implementation is the offs_boot offset. On resume from suspend, the timekeeping core estimates how long the system was suspended (from the persistent clock or the RTC) and adds that interval to offs_boot via tk_update_sleep_time() (timekeeping.c:168-176 v6.12), which advances offs_boot by the sleep delta and recomputes the monotonic_to_boot timespec used to accelerate the vDSO. So CLOCK_BOOTTIME = CLOCK_MONOTONIC + offs_boot, where offs_boot grows by the suspended duration on every resume. Use boottime when an interval must include time the machine was asleep — for example, a key-rotation deadline or a session timeout that should keep counting across a laptop lid-close. Plain monotonic would under-count by the sleep duration. The full suspend/resume interaction is in Suspend Resume and Timekeeping.


CLOCK_MONOTONIC_RAW — The Bare Hardware Rate

CLOCK_MONOTONIC_RAW, added in Linux 2.6.28, is “identical to CLOCK_MONOTONIC, except that it provides access to a raw hardware-based time that is not subject to NTP adjustments or the incremental adjustments performed by adjtime(3)” (clock_gettime(2)). The kernel docs put it: ktime_get_raw() is “Like ktime_get(), but runs at the same rate as the hardware clocksource without (NTP) adjustments for clock drift” (kernel docs).

The implementation distinction is the separate readout base tkr_raw in struct timekeeper, parallel to tkr_mono. While tkr_mono.mult is continuously steered by timekeeping_adjust(), tkr_raw.mult keeps the clocksource’s original calibrated mult and is never adjusted by NTP — the accumulation path advances tkr_raw.xtime_nsec using the unadjusted raw_interval (timekeeping.c:2259-2265 v6.12). The use case is narrow: measuring the intrinsic rate of the hardware clocksource itself, for instance to characterise oscillator drift or to feed a userspace discipline loop that wants the undisciplined signal. For ordinary “how long did this take” measurements, CLOCK_MONOTONIC is the right choice — the NTP slewing on monotonic is a feature (it keeps the second roughly correct), not a bug.


CLOCK_TAI — Atomic Time Without Leap Seconds

CLOCK_TAI (Linux 3.10) is “a system-wide clock derived from wall-clock time but counting leap seconds” — that is, it tracks International Atomic Time, which advances continuously and does not insert leap seconds (clock_gettime(2); the man page also notes it is “settable only indirectly by setting CLOCK_REALTIME”). The kernel docs describe its reader as “Like ktime_get_real(), but uses the International Atomic Time (TAI) reference instead of UTC to avoid jumping on leap second updates” (kernel docs).

The relationship is CLOCK_TAI = CLOCK_REALTIME + tai_offset, where tai_offset is the current UTC-to-TAI difference in whole seconds (37 seconds as of the last leap second in 2017, though this is administratively set, not hard-coded). The kernel maintains this as offs_tai, derived from offs_real plus the TAI offset (timekeeping.c:165 v6.12): tk->offs_tai = ktime_add(tk->offs_real, ktime_set(tk->tai_offset, 0));. Because TAI has no leap seconds, it is continuous across a leap-second event where CLOCK_REALTIME repeats or skips a second — making it the right wall-referenced clock for systems that must not see the leap-second discontinuity. The leap-second handling itself is in Leap Seconds and Clock Discontinuities.

Uncertain

Verify: the current UTC-to-TAI offset value of “37 seconds.” Reason: this is an administratively configured value set by adjtimex()/NTP and reflects the leap-second history through 2017; it is not compiled into the kernel and would change if a future leap second were declared (none has been as of this writing). To resolve: read tai_offset via adjtimex() (TIME_OFFSET / the tai field) on a synchronised system, or check the IERS leap-second bulletin. uncertain


The Durations Rule — Why It Bites

The recurring application bug is using CLOCK_REALTIME (or a language wrapper over it) to measure an elapsed interval:

struct timespec a, b;
clock_gettime(CLOCK_REALTIME, &a);   // WRONG for durations
do_work();
clock_gettime(CLOCK_REALTIME, &b);
double secs = (b.tv_sec - a.tv_sec) + (b.tv_nsec - a.tv_nsec) / 1e9;

If an NTP daemon or an administrator steps the wall clock backward during do_work(), b can be earlier than a and the measured duration goes negative — or if it steps forward, the duration is wildly inflated. A timeout computed this way can fire instantly or never. The fix is to use the monotonic spine:

struct timespec a, b;
clock_gettime(CLOCK_MONOTONIC, &a);   // correct for durations
do_work();
clock_gettime(CLOCK_MONOTONIC, &b);   // cannot have moved backward

The kernel guarantees, as shown in the do_settimeofday64 walk above, that CLOCK_MONOTONIC is untouched by wall-clock steps. This same hazard is why language runtimes capture a monotonic reading: Go’s time.Time embeds a CLOCK_MONOTONIC value so that t2.Sub(t1) is immune to wall-clock steering — see time.Time Comparison and the Monotonic Clock for how Go does it and the subtle ==-versus-Equal trap that follows. The lesson is identical at the kernel and language levels: wall-clock for display, monotonic for duration.


Choosing the Right Clock

  • Measuring how long something took, a timeout, backoff, rate limiting? CLOCK_MONOTONIC. It cannot step. Accept that it pauses during suspend.
  • Need the interval to keep counting while the machine is suspended? CLOCK_BOOTTIME.
  • Need the actual calendar date/time to display, log, or persist? CLOCK_REALTIME — and accept that it can jump; never use it for durations.
  • Characterising the hardware oscillator’s own drift, undisciplined? CLOCK_MONOTONIC_RAW.
  • Need a continuous wall-referenced clock that survives leap seconds (e.g. financial timestamping, distributed ordering)? CLOCK_TAI.
  • Need a timestamp cheaply and can tolerate up to ~one tick of staleness? the *_COARSE variant of whichever clock — it skips the live clocksource read.

Failure Modes and Common Misunderstandings

CLOCK_MONOTONIC is the raw hardware rate.” It is not — it is NTP-rate-disciplined. If you want the undisciplined rate you must ask for CLOCK_MONOTONIC_RAW. Conflating the two leads to incorrect drift measurements.

CLOCK_MONOTONIC includes suspend.” It does not; it stops during suspend. Code that uses monotonic for a wall-elapsed deadline (e.g. “expire this token 1 hour from now”) will under-count if the machine sleeps. Use CLOCK_BOOTTIME there.

CLOCK_REALTIME only moves forward.” It can step backward when the clock is set. Any logic that assumes realtime is monotonic (computing end - start and trusting the sign) is a latent bug that surfaces the first time NTP steps the clock.

“The monotonic epoch is comparable across machines, or even across reboots.” It is not — POSIX leaves the starting point unspecified, and on Linux it resets at boot. Monotonic values are only meaningful as differences within a single boot of one machine. For cross-machine or cross-reboot comparison you need realtime, boottime+a stored anchor, or TAI.

CLOCK_TAI is the same as CLOCK_REALTIME plus a constant 37.” The offset is administratively set, can be wrong if the system has not been told the current TAI offset (it defaults to 0 until NTP supplies it), and would change at a future leap second. Do not hard-code 37.


Production Notes

The monotonic-versus-realtime distinction is one of the most common quiet bugs across the industry, not just in C. The kernel’s own internal accessors mirror the userspace clocks one-to-one (ktime_getMONOTONIC, ktime_get_realREALTIME, ktime_get_boottimeBOOTTIME, ktime_get_rawMONOTONIC_RAW, ktime_get_clocktaiTAI), so the same discipline applies inside the kernel: subsystems that need a duration call ktime_get(), while those that stamp a wall time call ktime_get_real() (kernel docs). Database and distributed-systems engineers care about CLOCK_TAI and tightly-disciplined CLOCK_REALTIME because event ordering depends on a continuous, leap-second-free reference — Google’s Spanner “TrueTime” and the broader move to leap-second smearing exist precisely to dodge the CLOCK_REALTIME discontinuity that this note describes. For the kernel-side machinery that maintains the offsets and applies NTP steering, see The Timekeeping Core; for what actually happens at a leap second, see Leap Seconds and Clock Discontinuities.


See Also