The High-Resolution Timer Subsystem (hrtimers): How Linux Delivers Nanosecond-Precision Wakeups

2026-06-28

The classic Linux timer wheel is fast but coarse — it ticks at HZ (typically 250 or 1000 Hz), giving 1–4 ms resolution. That's fine for TCP retransmits and filesystem flushes, but useless for media playback, high-frequency trading, or nanosleep(1us). The hrtimer subsystem solves this with a completely separate data structure and a different hardware backend.

Each CPU owns a per-CPU red-black tree of pending hrtimers, keyed by absolute expiration time in nanoseconds. Insertion and removal are O(log n), but only one timer matters at a time: the leftmost node, which is the next to fire. The kernel programs the local APIC timer (or HPET on older systems) to generate a one-shot interrupt at exactly that absolute time. There is no wheel, no tick, no bucketing — the hardware fires precisely when the soonest timer is due.

Hrtimers come in four clock bases: CLOCK_MONOTONIC, CLOCK_REALTIME, CLOCK_BOOTTIME, and CLOCK_TAI. Each base has its own RB-tree on each CPU, because they advance at different rates (REALTIME jumps when you run ntpd, MONOTONIC never does). When the APIC fires, the handler walks all four trees, expires anything due, runs the callback (in softirq context or, if HRTIMER_MODE_HARD, directly in the interrupt handler), and reprograms the APIC for the next-soonest deadline across all bases.

Real-world example: when you call clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL) asking to wake at T+50µs, the kernel inserts an hrtimer into the per-CPU MONOTONIC tree, schedules the task out, and programs the APIC for T+50µs. perf sched typically shows wake latency of 2–10 µs on a tickless (nohz_full) core — the rest is interrupt-entry, context-switch, and cache-warm overhead. Without hrtimers, the same call would round up to the next jiffy (1 ms on HZ=1000), a 20× error.

Rule of thumb: if your timeout is shorter than 1000/HZ milliseconds (1 ms on most distros), the kernel must be using hrtimers — the legacy wheel literally cannot represent it. Verify with cat /proc/timer_list and look for .resolution: 1 nsecs and the active timers per CPU.

The catch: hrtimers cost more per insertion than a wheel slot, and reprogramming the APIC after each expiry is a few hundred cycles. That's why the kernel still uses the wheel for things like socket keepalives — millions of timers that will mostly never fire.

Key Takeaway: hrtimers replace the timer wheel's bucketed O(1) approximation with a per-CPU RB-tree plus a one-shot APIC interrupt, trading slightly higher per-timer cost for nanosecond-precise wakeups that the wheel fundamentally cannot deliver.

All newsletters