Every machine has at least two clocks, and confusing them causes a specific family of bug: rare, unreproducible, seasonal, and usually blamed on something else.
The wall clock answers "what time is it". It is a real-world calendar time, and it is adjusted — by NTP, by the user, by a hypervisor restoring a snapshot, by the machine resuming from sleep. It can jump forward. It can jump backward. It is the only clock that can tell you the date.
The monotonic clock answers "how long has it been". It counts up from an arbitrary origin, usually boot, at a rate the system does its best to keep constant. It cannot be set. It never goes backward. It cannot tell you the date and it is meaningless between machines.
The rule that follows is short: use the wall clock for timestamps, the monotonic clock for durations, and never subtract two wall-clock readings.
What the wall clock does that ruins measurements
A duration computed from wall-clock readings inherits every adjustment applied between them:
- NTP steps. A daemon that finds the clock badly wrong will step it rather than slew it, moving it by the full offset instantly. A step of a second is unremarkable. A step of hours happens on machines with a dead RTC battery or a bad first sync at boot.
- NTP slewing. Even in normal operation the clock is deliberately run slightly fast or slow to converge on the true time, so a measured second is not exactly a second.
- Manual changes. A user setting the clock, or a container starting with a wrong time and correcting.
- Suspend and resume. A laptop closed for two hours resumes with a wall clock that has moved two hours and a monotonic clock that may or may not have, depending on which monotonic clock you asked for.
- Virtual machine snapshots. Restore a snapshot and the wall clock leaps to the present.
The symptoms are recognisable once you know the cause: a request that reports a latency of negative four milliseconds, a cache entry that never expires because its computed age went negative, a rate limiter that lets everything through for an hour, a timeout that fires instantly, a "time remaining" estimate of eleven days. Every one of those is a subtraction of two wall-clock readings.
Daylight saving is a red herring here. Unix time and the Windows FILETIME epoch are both UTC-based and do not shift for DST — the shift is a presentation concern. What DST actually breaks is code that stores or computes with local time, which is a different mistake with the same root: treating a calendar rendering as an instant.
The Windows API, specifically
Windows exposes several clocks and the names do not make the distinction obvious.
| Call | Kind | Notes |
|---|---|---|
| GetSystemTimeAsFileTime | wall | 100-nanosecond units, but updated at the timer tick |
| GetSystemTimePreciseAsFileTime | wall | high-precision variant, Windows 8 and later |
| GetTickCount64 | monotonic | milliseconds since boot, tick-resolution |
| QueryPerformanceCounter | monotonic | high resolution; pair with QueryPerformanceFrequency |
Two historical traps live in that table.
The original 32-bit GetTickCount counts milliseconds in a 32-bit unsigned value, which wraps after 2^32 milliseconds — about 49.7 days. Code that compared tick counts with a plain subtraction worked perfectly for seven weeks and then failed on machines with long uptimes, which is to say on servers. GetTickCount64 exists to end this; unsigned subtraction also handles the wrap correctly, which is why the bug persisted so long in code that happened to be written the right way by accident.
QueryPerformanceCounter is the high-resolution monotonic source. Its frequency is fixed at boot and must be read with QueryPerformanceFrequency, not assumed. On modern hardware it is generally backed by an invariant timestamp counter and is documented as consistent across processors; the historical warnings about it drifting between cores relate to older hardware where the TSC was not invariant.
Timer resolution, and the power problem
Separate from clock precision is timer resolution: how often the system can wake a thread up.
Windows has traditionally run its scheduler tick at about 64 Hz, roughly every 15.6 milliseconds. That is the granularity of Sleep and of ordinary timers, which is why Sleep(1) typically sleeps for something between 1 and 16 milliseconds and never for exactly one.
For a long time any process could raise the tick rate globally by calling timeBeginPeriod(1), and multimedia software, browsers and games all did. The effect was system-wide: one application asking for a millisecond timer forced the whole machine to wake 1000 times a second instead of 64, which on a laptop is a measurable and entirely invisible battery cost. A machine could be pinned at high tick rates by a background tab. Later Windows versions narrowed this so that raising the period generally affects only the requesting process, with the scheduler no longer applying one application's demand to everything else.
The lesson generalises past Windows: a timer that fires more often than you need costs power in every system, and on mobile it is one of the larger drains an application can inflict without doing any work. Coalescing timers, and using event-driven waits rather than polling, is not a micro-optimisation.
The equivalents elsewhere
POSIX exposes the distinction directly through clock_gettime:
CLOCK_REALTIME— wall clock, settable, subject to NTP steps.CLOCK_MONOTONIC— monotonic since boot, adjusted by NTP slewing but never stepped.CLOCK_MONOTONIC_RAW— the same but not slewed, so it reflects the raw hardware rate rather than the corrected one.CLOCK_BOOTTIME— like MONOTONIC but continues to advance across suspend, which is usually what you want on a device that sleeps.
Language runtimes wrap the same pair, and the naming is fairly consistent once you know to look: wall clock in System.currentTimeMillis, time.time, Date.now; monotonic in System.nanoTime, time.monotonic and time.perf_counter, performance.now. Go is the pleasant exception — a time.Time from time.Now carries a monotonic reading alongside the wall reading, and subtracting two of them uses the monotonic part automatically, so the common mistake is difficult to make.
Measure your own browser's clocks
The same distinction is visible from JavaScript. Date.now is the wall clock, in whole milliseconds. performance.now is monotonic, relative to the page's time origin, and deliberately coarsened by the browser as a side-channel mitigation — the exact clamp depends on the browser and on whether the page is cross-origin isolated.
Press the button. Everything is measured locally in this tab and nothing is sent anywhere.
The setTimeout figure is the one that surprises people. Asking for zero delay does not give you zero: the specification requires nested timers to be clamped once you nest more than five deep, and background tabs are throttled far harder than that. Animation that depends on timer granularity should use requestAnimationFrame, which is tied to the display refresh rather than to a timer queue.
Leap seconds
UTC is kept within a second of the Earth's rotation by occasionally inserting a leap second, rendered as 23:59:60. Unix time has no representation for that value — it is defined as a count of seconds where every day has exactly 86,400 of them — so implementations must do something unphysical. Some repeat a second, so two distinct instants share a timestamp. Some step. Large operators instead smear the extra second across a window of many hours, running their clocks fractionally slow so that no discontinuity ever occurs, at the price of being deliberately wrong by up to a second during the smear.
The 2012 leap second is the canonical incident: a kernel bug turned it into a livelock in software that used high-resolution timers, and a number of large sites went down simultaneously for reasons that had nothing to do with their own code.
This is ending. In 2022 the General Conference on Weights and Measures resolved to stop inserting leap seconds by 2035, letting UTC drift from solar time instead. Until then, and for any timestamp already recorded, the ambiguity stands.
What to store
The measurement rules are only half of it. The storage rules:
- Store instants as UTC. A timestamp column with no zone, holding local time, is a data-loss bug waiting for an hour in autumn when the same local time occurs twice.
- Store future local events differently. A meeting at 09:00 next March in London is not an instant — it is a local time plus a zone, and the UTC instant it corresponds to is unknown until you know the DST rules that will be in force. Zone rules change by legislation with little notice, and the tz database is updated several times a year. Store the local time and the IANA zone identifier, and compute the instant when you need it.
- Use IANA zone identifiers, not offsets and not abbreviations. "+01:00" loses the rule. "CST" is ambiguous between several zones.
- Keep the tz database current. It is a data dependency with a release cadence, and treating it as static means being wrong the first time a government moves a transition date.
The short version
- Durations come from the monotonic clock. Always.
- Timestamps come from the wall clock, stored as UTC.
- Never subtract two wall-clock readings, and never trust a duration you did not compute yourself from a monotonic source.
- If a duration comes out negative, the clock moved; handle it rather than logging an impossible number.
- Do not raise the system timer resolution because a sleep felt imprecise.
None of this is difficult, and all of it is invisible until the day something adjusts a clock. That day is not predictable, which is exactly why the rule has to be followed when nothing is going wrong.