steady_clock vs system_clock vs high_resolution_clock: Measuring Elapsed Time and Timeouts

Key takeaways

Use steady_clock for elapsed time and timeouts and system_clock for timestamps. high_resolution_clock is an alias whose meaning differs between GCC, Clang and MSVC, so avoid it.

std::chrono::steady_clock (C++11) is the clock for measuring how long something took and for timeouts. Its one guarantee is that now() never goes backwards and advances at a steady rate, regardless of what happens to the wall clock.

#include <chrono>
#include <iostream>

int main() {
    auto start = std::chrono::steady_clock::now();
    // ... work ...
    auto elapsed = std::chrono::steady_clock::now() - start;
    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count()
              << " us\n";
}

What steady_clock guarantees, and what it does not

steady_clock::is_steady is true: successive calls to now() never decrease, and the tick rate does not change. That is all. The standard says nothing about what a steady_clock::time_point means. In practice it is time since boot, so:

  • it is useless for logging or for comparing timestamps between processes on different machines,
  • it resets when the machine restarts,
  • there is no way to turn it into a date. C++20 added clock_cast for clocks with a defined relationship (system_clock, utc_clock, tai_clock, gps_clock, file_clock), and steady_clock is deliberately not one of them.

What you get in return is exactly what elapsed-time code needs: if NTP or an administrator moves the wall clock back an hour, steady_clock does not notice. Under the hood, libstdc++ (GCC) and libc++ (Clang) on Linux read CLOCK_MONOTONIC via clock_gettime, and MSVC uses QueryPerformanceCounter.

All three report a period of std::nano, but that is the unit of the counter, not its accuracy. On the machine I tested with (TDM-GCC 10.3, MinGW-w64, Windows 11), steady_clock::period is 1/1000000000, yet the smallest step between two different now() values is 100 ns, and two back-to-back calls usually return the same value. If you need to know the real granularity of a platform, measure it rather than reading period:

#include <chrono>
#include <iostream>

int main() {
    using clk = std::chrono::steady_clock;
    long long min_step = -1;
    for (int i = 0; i < 1000; ++i) {
        auto a = clk::now(), b = a;
        while (b == a) b = clk::now();      // spin until the value changes
        long long s = std::chrono::duration_cast<std::chrono::nanoseconds>(b - a).count();
        if (min_step < 0 || s < min_step) min_step = s;
    }
    std::cout << "smallest observed step: " << min_step << " ns\n";
}

steady_clock vs system_clock vs high_resolution_clock

Clockis_steadyEpochUse for
steady_clocktrueUnspecified (usually boot)Elapsed time, timeouts, deadlines, benchmarks
system_clockfalseUnix epoch (specified since C++20)Timestamps, logs, expiry dates, anything shown to users
high_resolution_clockimplementation-defineddepends on the aliasNothing: use one of the other two

A useful rule: if the value answers “how long?”, use steady_clock. If it answers “when?”, use system_clock. Code that needs both, such as “retry after 30 seconds and log when the retry happened”, takes two readings, one from each clock.

The type system catches mixing them. Each clock has its own time_point type, so system_clock::now() - steady_clock::now() does not compile; GCC reports error: no match for 'operator-' (operand types are 'std::chrono::_V2::system_clock::time_point' ... and 'std::chrono::_V2::steady_clock::time_point' ...). What the compiler cannot catch is choosing the wrong clock for the job.

high_resolution_clock is an alias, and the aliases differ

The standard allows high_resolution_clock to be a typedef of steady_clock or system_clock, and the major standard libraries disagree:

Standard libraryhigh_resolution_clock isis_steady
libstdc++ (GCC, including MinGW)system_clockfalse
libc++ (Clang, Apple)steady_clocktrue
MSVC STLsteady_clocktrue

You can check your own build:

#include <chrono>
#include <iostream>
#include <type_traits>

int main() {
    using namespace std::chrono;
    std::cout << std::boolalpha
              << "steady_clock::is_steady:          " << steady_clock::is_steady << '\n'
              << "system_clock::is_steady:          " << system_clock::is_steady << '\n'
              << "high_resolution_clock::is_steady: " << high_resolution_clock::is_steady << '\n'
              << "high_resolution_clock == system_clock: "
              << std::is_same_v<high_resolution_clock, system_clock> << '\n';
}

With GCC 10.3 this prints true, false, false, true. The same benchmark code that uses high_resolution_clock is monotonic when built with Clang or MSVC and follows wall-clock adjustments when built with GCC. Since steady_clock already has nanosecond units on all three, there is no precision gain from high_resolution_clock.

This is a portability bug that hides well. A GCC build on a developer laptop almost never sees a clock step during a benchmark, so the problem shows up on long-running servers instead: an occasional absurd or negative latency in metrics that nobody can reproduce, because it only happens when NTP steps the clock during a measurement. Replacing high_resolution_clock with steady_clock in timing code is one of the first changes I make when reviewing it.

Measuring elapsed time correctly

Keep the native duration, convert only to display

#include <chrono>
#include <iostream>
#include <thread>
#include <utility>

template<class F>
std::chrono::steady_clock::duration time_it(F&& f) {
    auto t0 = std::chrono::steady_clock::now();
    std::forward<F>(f)();
    return std::chrono::steady_clock::now() - t0;
}

int main() {
    using namespace std::chrono_literals;
    auto d = time_it([] { std::this_thread::sleep_for(100ms); });

    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(d).count() << " us\n";
    std::cout << std::chrono::duration<double, std::milli>(d).count() << " ms\n";
}

On my Windows test machine this prints about 104,500 us: slightly more than 100 ms, never less. sleep_for guarantees a minimum duration, and the thread then has to be rescheduled. The overshoot is small on an idle Linux system and larger on Windows or under load. It is real elapsed time, and steady_clock measures it correctly, which is why a sleep is a poor way to sanity-check a timer.

Two rules about units:

  • count() returns ticks in the duration’s own period. (end - start).count() printed with an “ns” label is correct only because the major implementations use nanoseconds. Always duration_cast (truncating) or convert to a floating-point duration before printing.
  • C++20 added operator<< for durations, which prints the unit suffix (5ms). GCC 10 does not implement it (no match for 'operator<<'), so use count() with a cast if you must support older toolchains.

A reusable stopwatch

class Stopwatch {
    std::chrono::steady_clock::time_point start_ = std::chrono::steady_clock::now();
public:
    void reset() { start_ = std::chrono::steady_clock::now(); }
    std::chrono::steady_clock::duration elapsed() const {
        return std::chrono::steady_clock::now() - start_;
    }
};

Returning steady_clock::duration rather than milliseconds lets the caller decide the unit and avoids truncating short intervals to zero.

Narrow types overflow

A signed 64-bit count of nanoseconds lasts about 292 years, so steady_clock itself does not overflow in practice. Converting to a narrower representation does: duration<int, std::milli> overflows after about 24.8 days (2^31 ms), which is a real bug in long-running processes that store uptimes or timeouts as int.

Timeouts and deadlines

For timeouts, compute a deadline once and compare against it, instead of recomputing “start + budget” in several places:

#include <algorithm>
#include <chrono>
#include <thread>

template<class Pred>
bool poll_until(std::chrono::steady_clock::time_point deadline, Pred ready,
                std::chrono::milliseconds interval = std::chrono::milliseconds(10)) {
    while (!ready()) {
        auto now = std::chrono::steady_clock::now();
        if (now >= deadline) return false;   // timed out
        std::this_thread::sleep_for(
            std::min<std::chrono::steady_clock::duration>(interval, deadline - now));
    }
    return true;
}

// usage: poll_until(std::chrono::steady_clock::now() + std::chrono::seconds(1), [&] { return done.load(); });

Clamping the sleep to deadline - now keeps the function from overshooting the deadline by a whole interval. When another thread can signal you, prefer blocking on a std::condition_variable, std::future or C++20 std::stop_token over polling.

Which clock timed waits use

condition_variable::wait_for(lock, 5s) is specified as wait_until(lock, steady_clock::now() + 5s), so wall-clock changes should not stretch or shorten the wait. Older implementations did not always manage that: with libstdc++ before GCC 10 on Linux, a steady_clock deadline was converted to the realtime clock internally, so changing the system time could make a timeout fire early or hang. GCC 10 with glibc 2.30 or newer fixed this by using pthread_cond_clockwait. If your code runs on older distributions and depends on precise timeouts, test it with a clock change.

A deadline expressed as a system_clock time point, by contrast, should follow wall-clock changes: “wake me at 09:00” means 09:00 on the wall clock even if it was just corrected. So sleep_until(system_clock::...) for scheduled times of day, steady_clock for “in 30 seconds”. The clock of a deadline is a statement of intent.

Suspend, sleep and virtual machines

On Linux, CLOCK_MONOTONIC stops while the system is suspended:

auto start = std::chrono::steady_clock::now();
// laptop lid closed for 10 minutes here...
auto elapsed = std::chrono::steady_clock::now() - start;   // does NOT include the 10 minutes on Linux

A “5-minute session timeout” measured with steady_clock therefore does not expire during a laptop’s sleep. Linux has CLOCK_BOOTTIME, which includes suspend, but no standard clock exposes it, so read it with clock_gettime directly. For servers this rarely matters; for desktop and mobile apps, decide explicitly whether a timeout should include time spent asleep, and test it on each target OS rather than assuming.

Virtual machines add a similar wrinkle: a paused or live-migrated VM can resume with a jump or a slewed correction. Monotonicity still holds, but “elapsed” can differ from what an outside observer would measure.

Benchmarking with steady_clock

steady_clock is the right clock for a hand-rolled benchmark, but the clock is rarely the source of error:

  • Timer overhead and granularity. What you time must take much longer than the clock’s real step (100 ns on the MinGW build above). Time a loop of many iterations and divide.
  • Warm-up. The first run pays for page faults, cache misses and lazy initialization.
  • Repeat and use the median. A single run is dominated by scheduling noise.
  • Dead-code elimination. At -O2 the compiler can delete work whose result is unused, and you end up timing an empty loop. Use a harness such as Google Benchmark with benchmark::DoNotOptimize, or at least consume the result.

For anything below a microsecond or so, use a real benchmark framework or a profiler.

FAQ

Should I use steady_clock or system_clock?

If the value answers “how long?” (elapsed time, timeouts, benchmarks), use steady_clock: it never goes backwards. If it answers “when?” (log lines, expiry dates, database columns), use system_clock, which tracks wall-clock time and can jump when NTP or an administrator changes it.

Is high_resolution_clock more precise than steady_clock?

No. It is an alias: of system_clock in GCC’s libstdc++ (so is_steady is false) and of steady_clock in Clang’s libc++ and MSVC. steady_clock already reports a 1 ns period on all three, so use it directly.

How do I print a steady_clock duration in milliseconds?

Convert first: duration_cast<milliseconds>(end - start).count() for an integer, or duration<double, std::milli>(end - start).count() for a fractional value. Calling count() on the raw difference gives ticks of the clock period, which is not guaranteed to be nanoseconds.

Can I convert a steady_clock time_point to a date or save it to a file?

No. Its epoch is unspecified (typically system boot), and clock_cast deliberately has no conversion for it. Record a system_clock time point alongside it if you need a human-readable time.

Does steady_clock keep counting while the computer is asleep?

On Linux, libstdc++ and libc++ use CLOCK_MONOTONIC, which stops during suspend, so a timeout measured with steady_clock does not advance while a laptop sleeps. Use clock_gettime(CLOCK_BOOTTIME) directly if suspend time must count.