Windows timer resolution, measured — not assumed.
Measure the timer resolution Windows actually delivers, then request the best one.
Windows reports a nominal range, but the latency a process really sees is quantised
and not monotonic: the nominal minimum is often slower than a slightly coarser
request. ticktune sweeps the usable range, measures real Sleep(1) latency at each
step, and recommends the point with the lowest consistent latency — or the middle
of the coarsest band that stays within a latency budget.
Single static executable. No dependencies, no driver, no service.
- Measures, does not assume. Every candidate is timed with
Sleep(1)onQueryPerformanceCounter, and the scan reports the distribution, not one number. - Finds the real knee. A fixed 100 ns sweep over the first 50 µs above the floor catches the narrow fast band that a coarse step can walk straight past.
- Latency or power.
--prefer latencyminimises p50 under the p95 cap;--prefer powertakes the coarsest band that still meets the latency budget, and its middle. - Two modes. Per-process by default (no admin needed); machine-wide only after
an explicit
ticktune global onand a reboot. - Nothing hidden. The registry key and the dynamic-tick setting are reported, never toggled behind your back.
ticktune queryThis prints the finest and coarsest periods, the current global tick, the request
mode, the dynamic-tick state, the installed logon task and whether you are
elevated. It changes nothing; without elevation the dynamic-tick line reads
unknown, because reading it uses bcdedit.
- Per-process (default). A timer request belongs to the process that makes it
and disappears when that process exits. It is not inherited:
ticktune applytherefore only affectsticktune.exeitself — not a child process, and not an already-running game or browser. - Machine-wide. After
ticktune global on(elevated) and a reboot, a request affects every process — but only while some process holds one.global onalone changes nothing, so keep a holder running: the logon task from step 6, or a foregroundticktune apply. Confirm it withticktune verify-globalbefore relying on it.
# elevated terminal
ticktune global on
# reboot, then
ticktune verify-globalverify-global needs the timer at rest: if anything already holds a fine request
(a browser, an audio app, or the logon holder from step 6), it reports
inconclusive and names the tick. Stop that process and rerun.
Close browsers, launchers, overlays, monitoring and RGB utilities. Some of them hold their own timer requests, which can prop up the timer for every other process. Plug in a laptop, keep the power plan fixed between runs, and avoid benchmarking under load. For unattended runs, Windows can also demote the app to "Medium" QoS after a period without input — see the Before you benchmark note below.
ticktune benchThe scan takes two to three minutes and prints a report like this example (the plateau boundaries and point counts vary by machine):
ticktune bench
range 0.5000 - 15.6250 ms
global tick 1.0000 ms
curve (median p50 per plateau)
0.5000 - 0.5105 ms p50 1.505 ms 106 points
0.5106 - 0.5132 ms p50 1.019 ms 27 points <- recommended
0.5133 - 1.0000 ms p50 1.518 ms 413 points
result
recommend 0.5119 ms (5119 x 100ns)
band 0.5106 - 0.5132 ms (27 values, all equivalent)
latency min 1.008 p50 1.017 p95 1.517 max 1.525 ms
vs floor p50 1.505 ms at 0.5000 ms (32% lower)
stability stable (re-check agreed)
next step ticktune apply --resolution 5119
The curve is collapsed into plateaus because 500+ identical rows are noise. Add
--rows to see every measured period. When several neighbouring periods measure
the same, the tool reports that band and recommends its middle: the edges of a
plateau are marginal by definition, so repeated runs agree on the middle and only
the edge rows wobble. stable means a re-check of the winner agreed with the first
pass; unstable means the machine was noisy — rerun when it is quieter.
--samples 100 gives a quicker, noisier answer; the default 200 is two to three
minutes, and the cap of 1000 is about fourteen. Press ESC to stop early — the
recommendation then covers the points measured so far. Use --prefer power if you
would rather have the coarsest band inside the latency budget (p50 ≤ 1.35 ms and
p95 ≤ 2.0 ms); it recommends the middle of that band, and falls back to the latency
pick if nothing qualifies. No point is ever recommended unless
interference ≤ 2 %, p50 ≥ 0.99 ms and p95 ≤ 2.5 ms.
interference is the share of samples that overshoot the median by more than 1 ms.
The plateau view omits it; --rows prints it as a percentage. The scan runs at
time-critical priority, so a normal application may see somewhat more interference
than this figure.
ticktune apply --resolution 5119apply prints the period it requested and the p50 it actually measured, because
Windows can report success and still ignore a request outside the honoured band.
Keep the window open. The request belongs to this process alone, so ticktune measure
in another terminal does not see it; to observe the effect system-wide, enable
global mode (§2) and use ticktune verify-global. Press ESC or Ctrl+C (or close the
window) to release it — no cleanup, no leftover state. With --no-console there is
no Ctrl+C, so use ESC.
ticktune apply --resolution 5119 --persistThis registers a Task Scheduler task named ticktune that runs the holder 15 s
after logon, starts it immediately, and returns — the task, not your shell, owns
the request from then on. It waits up to 2 s for the holder's request to appear
and warns instead of claiming success if it does not. It runs as you, without
elevation and without a
console window. S4U needs the "Log on as a batch job" right, which administrators
have; without it the task is created but never starts. It is allowed on battery
and never times out (the default 72 h
limit would stop it). It is started with --no-esc, because ESC is read
desktop-wide and would otherwise end the holder from any window; stop it from
Task Scheduler instead. Delete it with ticktune startup remove, which also
ends a running holder first, and warns if the tick is still raised afterwards.
--persist is refused unless
GlobalTimerResolutionRequests is 1 (in per-process mode the holder would
only accelerate itself); that check reads the registry value, not the running
boot, so reboot and run ticktune verify-global (step 2) first. Check the task
after the next logon with ticktune status. It stores the exe path, so re-run
--persist after rebuilding, moving, or upgrading the binary — query marks
an outdated task as stale. A task that lacks this build's marker in its
description is refused by both --persist and startup remove; delete it by
hand with schtasks /Delete /TN ticktune /F.
ticktune startup remove # delete the logon holder
ticktune global off # elevated; takes effect after a rebootAny holder that is still running is ended with the task. The command warns if the tick is still at or below that period afterwards, which means Windows refused to end the holder (or something else holds it); only a reboot clears a surviving holder. No other state is left behind.
| Command | What it does |
|---|---|
ticktune query |
Print the finest/coarsest periods, global tick, mode, dynamic-tick state and logon task |
ticktune bench [--samples N] [--prefer latency|power] [--rows] |
Scan and recommend |
ticktune apply --resolution <100ns> |
Hold a period for this process; add --persist for the logon holder |
ticktune apply --ms <float> |
Same, in milliseconds |
ticktune status |
Sample this process, then print the query block |
ticktune measure [--samples N] |
Sample only; sets nothing (default 1000, max 100000) |
ticktune verify-global |
Check whether a request affects other processes |
ticktune global on|off |
Write the registry key (elevated, reboot required) |
ticktune startup remove |
Delete the logon holder |
ticktune help |
Print the built-in help (-h/--help works after any command) |
Values are in 100 ns units; the output always shows milliseconds too.
Exit codes: 0 success, 1 runtime failure (a failed Windows call, no usable
range or measurement, or a second bench/verify-global already running), 2 bad
command line or argument.
ticktune targets Windows 10 2004 (build 19041) and later, including Windows 11:
earlier builds do not have the per-process request model this tool assumes.
Since Windows 10 2004 a timer-resolution request is per-process: it applies to
the process that made it and dies with it, unless
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\kernel\GlobalTimerResolutionRequests
is 1 (read at boot, needs a reboot).
Sleep(1) granularity is also quantised and non-monotonic. A 0.500 ms request
typically sleeps ~1.50 ms, while a 0.512 ms request sleeps ~1.02 ms. Finer is not
better; the only way to know is to measure.
The knee moved between builds: 5023 (100 ns units) on 26100, 5018 on 26200/26300 —
where 0.5000 ms is actually on the slow three-tick side, and the fast window is
under 1 µs wide. The scan therefore walks that band one 100 ns unit at a time
instead of trusting a hardcoded value.
On some systems only a narrow band is honoured: a request a few microseconds above
it silently falls back to the 0.5 ms tick, i.e. the slow three-tick case again. That
is why bench prints the whole curve rather than a single number.
Windows 11 (2025+) can quietly drop a foreground app to "Medium" QoS after a period
with no input, which skews an unattended run — Microsoft's own docs warn about it.
Set DisableUserPresenceQos (REG_DWORD 1) under
HKLM\SYSTEM\CurrentControlSet\Control\Power\PowerThrottling before benchmarking.
ticktune never writes it.
- No
bcdedit.disabledynamictickis documented as a debugging switch and is widely reported to make machines unusable (minutes per UI action), and it needs a reboot to undo.query/statusreport it when run elevated, with the revert command if it is disabled; nothing writes it. - No automatic registry writes.
GlobalTimerResolutionRequestsis reported byquery/statusand only changed by an explicitticktune global on|off(which refuses to run unelevated). - No hardcoded "best" value. The result is per machine, BIOS, power plan and
driver set. Run
benchagain after a platform change.
On Windows, a normal release build is enough (MSVC or GNU toolchain):
cargo build --release
# target\release\ticktune.exePut that directory on PATH, or replace ticktune in the examples with
.\target\release\ticktune.exe.
From Linux, install the mingw-w64 linker, then cross-compile; no MSVC, no runtime DLLs:
rustup target add x86_64-pc-windows-gnu
sudo apt install gcc-mingw-w64-x86-64 # Debian/Ubuntu; the CI build uses this package
cargo build --release --target x86_64-pc-windows-gnu
# target/x86_64-pc-windows-gnu/release/ticktune.exe| file | role |
|---|---|
src/stats.rs |
pure statistics, grids, recommendation — unit-tested on any OS |
src/measure.rs |
Sleep(1) sampler on QPC |
src/scan.rs |
dense low band, coarse sweep, stability re-check |
src/nt.rs |
NtSetTimerResolution / NtQueryTimerResolution, registry, BCD probe |
src/persist.rs |
logon task in Task Scheduler (schtasks) |
src/main.rs |
CLI |
MIT — see LICENSE.