Skip to content
WindowsDeep Dive Published Updated 7 min readViews unavailable

Windows Timing APIs: Use QPC for Intervals and UTC for Timestamps

Use QueryPerformanceCounter for elapsed intervals and GetSystemTimePreciseAsFileTime for UTC event stamps, with explicit precision and sync limits.

A performance measurement and an event timestamp answer different questions. “How long did this operation take?” needs a stable difference clock. “At what UTC time did this event occur?” needs a clock tied to the system’s time-of-day. Windows exposes both, but the high-resolution APIs are not interchangeable: use QueryPerformanceCounter (QPC) for intervals and GetSystemTimePreciseAsFileTime for UTC timestamps.

The examples below are native Win32 C++ and follow Microsoft’s current high-resolution timestamp guidance. QPC and QueryPerformanceFrequency are available starting with Windows 2000; the precise UTC function requires Windows 8 or Windows Server 2012. A requirement to support older systems needs a separate, explicitly lower-resolution UTC path.

QPC is a difference clock, not a date

QueryPerformanceCounter returns a high-resolution count suitable for measuring intervals on the same Windows system or virtual machine. Its value has no UTC epoch, is not synchronized to an external time reference, and should not be written into a log column labeled as a date. It is independent of time-zone settings, daylight-saving changes, and administrative changes to the wall clock.

Query the frequency once when the application initializes. Microsoft documents the QPC frequency as fixed at system boot and consistent across processors, so it can be cached for that running system. It is a conversion factor in counts per second, not necessarily a raw hardware oscillator rate. Do not assume it will be the same on another machine, another boot, or another virtual machine.

The example keeps values as integers until after it subtracts the starting counter, checks the expected ordering, and then converts the interval to seconds:

#include <windows.h>
#include <cstdio>
#include <stdexcept>
#include <system_error>

static LONGLONG QueryCounterOrThrow()
{
    LARGE_INTEGER value{};
    if (!QueryPerformanceCounter(&value)) {
        const DWORD error = GetLastError();
        throw std::system_error(static_cast<int>(error),
                                std::system_category(),
                                "QueryPerformanceCounter");
    }
    return value.QuadPart;
}

static void DoBoundedWork()
{
    // Replace this body with the representative operation being measured.
}

int main()
{
    try {
        LARGE_INTEGER frequency{};
        if (!QueryPerformanceFrequency(&frequency)) {
            const DWORD error = GetLastError();
            throw std::system_error(static_cast<int>(error),
                                    std::system_category(),
                                    "QueryPerformanceFrequency");
        }
        if (frequency.QuadPart <= 0) {
            throw std::runtime_error("invalid QPC frequency");
        }

        const LONGLONG start = QueryCounterOrThrow();
        DoBoundedWork();
        const LONGLONG finish = QueryCounterOrThrow();

        // QPC is non-decreasing. Reject unexpected signed values before subtracting.
        if (start < 0 || finish < start) {
            throw std::runtime_error("invalid QPC interval");
        }
        const LONGLONG ticks = finish - start;
        const double seconds =
            static_cast<double>(ticks) /
            static_cast<double>(frequency.QuadPart);
        (void)seconds; // Record or report the measurement in the application.
    } catch (const std::exception& error) {
        std::fprintf(stderr, "%s\n", error.what());
        return 1;
    }
    return 0;
}

The guarded subtraction avoids signed overflow for the non-negative counter values used by this interval example. Converting only the delta avoids throwing away small differences by converting two large absolute counter values first. Do not multiply a long-running tick count by one million in a 64-bit integer without an overflow analysis. If the code needs exact sub-tick accounting or very long intervals, retain the integer count and perform checked or ratio-based conversion appropriate to its data model.

Handle a failed API call before doing other work if diagnostics need GetLastError; Microsoft notes that valid calls succeed on supported Windows systems, but checking the return value keeps the example’s failure path explicit. Do not try to “fix” QPC by pinning threads to one CPU or reading the processor timestamp counter directly. Windows selects and manages the underlying counter; direct TSC reads reintroduce hardware, migration, and virtualization assumptions that QPC abstracts.

QPC resolution is not the same thing as accuracy or the smallest reliably measurable operation. Counter-read overhead, hardware timer selection, scheduling, virtualization, and the operation itself all affect a benchmark. Measure representative work, repeat it, and report a distribution rather than claiming that a displayed fractional digit is guaranteed accuracy.

Use UTC for event timestamps

For a high-resolution system time-of-day reading, call the UTC API directly:

FILETIME eventUtc{};
GetSystemTimePreciseAsFileTime(&eventUtc);

This function returns the current system date and time as UTC in a FILETIME; it has no return status. A FILETIME encodes a 64-bit count of 100-nanosecond intervals since 1601-01-01 UTC. Keep it as a FILETIME or convert it with the documented Windows time conversion APIs. Do not print its integer count as if it were Unix seconds or local time.

“Precise” describes the API’s resolution, not the correctness of the machine’s clock against an external authority. A timestamp is only as well synchronized as the system time service and its configured reference. For cross-machine correlation, synchronize each host under an appropriate time policy, record UTC, and account for residual offset and uncertainty. Do not compare QPC readings from separate machines: their origins are not shared.

The UTC clock can also be adjusted or disciplined as time synchronization operates. Use QPC for an elapsed measurement even if you also capture a UTC event stamp around it. Do not subtract two wall-clock readings to benchmark a short operation; an adjustment can distort that interval. Conversely, do not convert a QPC value into UTC by adding it to an arbitrary FILETIME. The counters have different epochs and no API-defined one-to-one mapping.

Choose by purpose and version

Use QPC when the events belong to one system and the result is an elapsed interval, latency, or profiler timestamp. It is monotonic and includes the time the system spends in sleep states according to Microsoft’s guidance; choose a different clock only when a documented requirement says suspend time must be excluded or included differently.

Use GetSystemTimePreciseAsFileTime when the value is a UTC time-of-day label and sub-microsecond resolution is useful. The API is documented for Windows 8 and later clients and Windows Server 2012 and later servers. If an application must run on earlier Windows versions, choose and document an available lower-resolution UTC API or resolve the function dynamically and handle its absence. Do not silently call an unavailable export.

If 10-to-16-millisecond resolution is sufficient, Microsoft’s guidance lists GetTickCount64 among the choices for non-UTC time stamps and GetSystemTimeAsFileTime for UTC. These APIs provide lower resolution; replacing one with another changes the contract. A clock’s name, integer width, or number of printed digits does not establish monotonicity, synchronization, or precision.

Troubleshoot the measurement instead of its clock label

If an interval is negative or implausibly large, check the start and end values, cached frequency scope, integer conversion, and whether values were mixed across a reboot or VM boundary. Keep frequency associated with readings from the same running system. If readings from different threads differ by one tick, Microsoft’s guidance treats their ordering as ambiguous at that granularity; do not use a single tick as a causal proof. For legacy Windows 2000/XP support, Microsoft’s guidance also documents rare cross-processor consistency failures on systems with flawed firmware or unsynchronized/non-invariant TSCs. Treat that as a platform-support issue, not a reason to pin threads or read the TSC directly.

When a UTC timestamp disagrees across hosts, inspect each host’s synchronization status and time source rather than expecting QPC to reconcile them. When UTC ordering is correct but a duration looks erratic, keep both data types: UTC for the audit event and QPC delta for the measurement. They can be logged together with machine identity and clock-sync context, but one is not derived from the other.

For a benchmark report, store the measured QPC tick delta, the frequency used, converted unit, OS/VM context, workload version, and repeated-run statistics. For an audit event, store UTC FILETIME or a correctly converted UTC representation, plus whatever synchronization provenance the audit policy requires. Do not label a monotonic counter “timestamp” without stating its origin and scope.

The native sample is source-reviewed but was not compiled or executed on this macOS authoring host. Its API contracts and minimum-version notes follow Microsoft’s Win32 documentation; validate the build and runtime behavior on each Windows family that the application actually supports.

Related:

Sources:

Comments