The fastest Linux timestamps

In low-latency systems, even simple timestamping can become a bottleneck. This article explores the inner workings of TSC and vDSO on Linux to minimize overhead for performance measurement tasks.
The fastest Linux timestamps
26 Apr 2026
Timing the timers
One of my pet projects at my last job was to introduce distributed tracing to a low-latency pipeline (think 1–10 microseconds per stage) using OpenTelemetry. As part of this effort I spent a considerable amount of time designing, implementing, and optimising our own C++ tracing client library, as the official one has too much overhead. My goal was for the latency impact per component to stay under 5% so both developers and users would feel comfortable leaving traces always on in production; this translated to a budget of about 50–100 ns (a few hundred clock cycles) per span.
As you might imagine, at this scale you must carefully consider every aspect of the design and implementation, from ID generation to serialisation. One of these not-so-small details is how to timestamp spans. OTLP uses two time fields, one each for the start and end of the span as measured by the local wall clock. Although the end time is an absolute timestamp, it’s expected that it will always be later than the start time, as its primary purpose is to measure the span duration. The official client handles this roughly as:
Span::Span(/* ... */)
{
// ...
start_time_ = std::chrono::system_clock::now();
start_steady_time_ = std::chrono::steady_clock::now();
// ...
}
void Span::End(/* ... */)
{
// ...
auto end_steady_time = std::chrono::steady_clock::now();
auto duration = end_steady_time - start_steady_time_;
end_time_ = start_time_ + duration;
// ...
}
It takes the start time from the real-time clock and uses two timestamps from the monotonic clock to calculate a nonnegative span duration without interference from discontinuous system clock adjustments. The end time is a synthetic timestamp obtained by adding the duration to the start time, rather than directly from any clock.
Does querying the system clocks three times per span have any significant performance impact? If you’re at all familiar with Linux internals, you might expect the answer to be no: after all, in practically any application using the C library the clock_gettime() syscall (indirectly called by the now() functions) will be routed through vDSO to avoid context-switching into the kernel. Let’s do a quick benchmark to confirm:
void BM_NaiveBackToBack(benchmark::State& state)
{
for (auto _ : state) {
auto ts = std::chrono::system_clock::now();
auto start = std::chrono::steady_clock::now();
auto duration = std::chrono::steady_clock::now() - start;
benchmark::DoNotOptimize(ts + duration);
}
}
On my laptop this yields iteration times between 46 and 49 ns—almost our entire time budget for a span, spent just on timestamping! Clearly this will not suffice.
If we’re to meet our latency constraints, we’ll need to understand how Linux clocks work under the hood and find out how much weight we can shed. We’ll see what the x86 timestamp counter is and how it works, do a deep dive into the implementation of vDSO, and use our newly acquired knowledge to chop over 50% of the timing overhead from our initial attempt.
The TSC
Almost anyone who has written microarchitecture benchmarks or otherwise needed fast and accurate timestamps on x86 platforms is well acquainted with the CPU’s timestamp counter, or TSC. Quoting from Intel’s System Programming Guide:
The time-stamp counter […] is a 64-bit counter that is set to 0 following a RESET of the processor. Following a RESET, the counter increments even when the processor is halted by the HLT instruction or the external STPCLK# pin.
The time stamp counter in newer processors may support an enhancement, referred to as invariant TSC. […] The invariant TSC will run at a constant rate in all ACPI P-, C-. and T-states. This is the architectural behavior moving forward. On processors with invariant TSC support, the OS may use the TSC for wall clock timer services (instead of ACPI or HPET timers). TSC reads are much more efficient and do not incur the overhead associated with a ring transition or access to a platform resource.
An invariant TSC behaves exactly as you would expect from a clock: it’s fully synchronised across cores, runs at a constant rate independent of frequency scaling, and doesn’t stop even when the system is idle or suspended.
Although TSC reads are in fact much more efficient than the alternatives, they’re not free. The cost of reading the TSC is twofold: the instruction itself is slow (rdtsc has a reciprocal throughput of 25 core clock cycles on Skylake) and the instruction stream must be serialised first, either through an explicit lfence or by using rdtscp (32 cycles).
When syscalls aren’t
The long and short of it is that when a user-space process invokes certain system calls through the C library (most notably, clock_gettime()), the library directs that call to a small shared library mapped into the process (the vDSO), which avoids the overhead of switching into ring 0 by reading the required information from a memory region shared with the kernel, called the vvar or data page.
The data page contains vdso_data structures. We’re particularly interested in the do_hres() function, which is used for all calls to high-resolution clocks such as CLOCK_REALTIME and CLOCK_MONOTONIC. The way it works is as follows: on every kernel tick, the CPU responsible for updating the timers updates the data page with the resulting values. This is the kernel’s best estimate of the current time, along with the current cycle count of the underlying clock source (the TSC) and a multiplier/shift pair to efficiently convert cycles into nanoseconds. The whole structure is protected by a seqlock.
Source: Hacker News















