tokio-osinterval
An alternative to tokio::time::Interval that drives its periodic ticks
from the operating system's native async-capable timer facility instead of
tokio's userspace timer wheel.
The goal is more accurate, lower-jitter periodic ticks (especially for
sub-ms to low-ms periods) while keeping a familiar Interval-shaped API.
Why
tokio::time::Interval runs on top of tokio's coarse timer wheel
(default ~1 ms resolution). For most uses that is fine; for tight
heartbeats, audio/MIDI clocks, periodic polling under jitter budgets, or
sub-ms work, the wheel's slot size becomes the dominant source of error.
OsInterval instead asks the kernel:
| Platform | Backend | tokio integration |
|---|---|---|
| Linux / Android | timerfd_create(CLOCK_MONOTONIC, …) |
tokio::io::unix::AsyncFd |
| macOS / iOS / *BSD | kqueue + EVFILT_TIMER (NOTE_NSECONDS) |
tokio::io::unix::AsyncFd |
| Windows 10 1803+ / Server 2019+ | CreateWaitableTimerExW(HIGH_RESOLUTION) |
CreateThreadpoolWait callback → atomic + Waker |
| Windows (older) | CreateThreadpoolTimer (auto-fallback) |
callback → atomic + Waker |
| Other | tokio::time::sleep_until fallback |
n/a |
Disabling the os-native feature also forces the sleep_until
fallback on platforms that would otherwise use a native backend.
On Windows, the high-resolution waitable timer (Win10 1803+ /
Server 2019+) is detected once per process via a runtime probe; older
Windows versions transparently use the threadpool-timer path. Either
way, OsInterval owns one kernel object per instance — no shared
global reactor beyond what tokio already provides.
Quick example
use Duration;
use interval;
async
The API mirrors tokio::time::Interval closely:
use Duration;
use ;
use Instant;
# async
Available methods: tick, poll_tick, period, missed_tick_behavior,
set_missed_tick_behavior, reset, reset_immediately, reset_after,
reset_at. See the API docs for
details.
MissedTickBehavior
Identical semantics to tokio::time::MissedTickBehavior:
Burst(default) — fire as fast as possible until caught up.Delay— slip the schedule: next tick isperiodafter the missed tick was observed.Skip— keep the original schedule, snapping the next deadline to the next aligned multiple ofperiod.
The userspace policy is shared by every backend; the kernel timer is re-armed each tick rather than running in periodic mode, so behavior is identical across platforms. See the Design section below for the rationale.
Design: one-shot re-arm, not kernel-periodic
Every supported backend (timerfd, EVFILT_TIMER, waitable timer,
threadpool timer) can be configured as a true periodic timer that the
kernel re-fires on its own. OsInterval deliberately doesn't do that.
Each tick is a fresh one-shot arming computed in userspace from the
current MissedTickBehavior and the previous deadline.
This costs one extra syscall per tick (a few microseconds on Linux/BSD, negligible above ~1 ms periods). In exchange:
-
Uniform
MissedTickBehavioracross platforms. OnlyBurstmaps cleanly to a kernel-periodic timer — and even then, Windows timers don't expose an overrun count, so a periodic-mode implementation would still hand-roll the count there.DelayandSkiprequire a re-arm at every tick to slip or snap the schedule, so periodic mode would mean two divergent code paths per backend. With one-shot re-arm, one userspace policy module drives all three behaviors identically on every target. -
interval_at(start, period)works portably.EVFILT_TIMERhas no cross-platform "fire once at T, then every P" mode (Apple'sNOTE_ABSOLUTEand FreeBSD'sNOTE_ABSTIMEare spelled differently and use OS-specific clock references). Computing each deadline in userspace side-steps that entirely. -
No background wakeups while idle. If the consumer holds an
OsIntervalbut doesn't calltick()for a while (awaiting something else), nothing fires in the kernel. A periodic timer would keep queuing expirations and producing reactor wakeups for ticks no one is observing. -
Per-tick rounding correction. On platforms where the kernel timer has coarser resolution than
Duration(NetBSD/OpenBSD round to whole milliseconds), each re-arm re-anchors against the original schedule (prev + period) so rounding error doesn't accumulate over thousands of ticks. -
Simple cancel-safety and
reset_*.tick()only advances state on a successful expiration read, so dropping the future preserves the next deadline.reset,reset_after,reset_at, andreset_immediatelyare just userspace deadline updates plus a lazy re-arm on the next poll — no special cases for "the kernel is in mid-period".
The headline downside — an extra timerfd_settime / kevent /
SetWaitableTimer per tick — is the dominant cost only at sub-100 µs
periods, which is below the realistic precision floor of every supported
OS scheduler anyway. For the periods this crate is designed for
(sub-millisecond up through low-millisecond), kernel jitter dwarfs the
re-arm cost.
Comparing precision vs tokio::time::Interval
The included criterion bench measures total elapsed time for N ticks at a small period. Lower mean = less drift; tighter samples = less jitter.
Sample run on macOS (Apple Silicon, kqueue backend, single-threaded runtime):
| Bench | OsInterval |
tokio::time::Interval |
Ideal |
|---|---|---|---|
| 50 ticks @ 2 ms | 100.4 ms | 100.9 ms | 100 ms |
| 100 ticks @ 500 µs | 50.1 ms | 51.2 ms | 50 ms |
Numbers vary with platform and scheduler load. On Windows 10 1803+ the high-resolution waitable timer typically delivers per-tick drift in the 300–600 µs range; on older Windows versions the threadpool-timer fallback is bounded by the system tick (~15.6 ms by default).
Cargo features
| Feature | Default | Effect |
|---|---|---|
interval |
✅ | Enables OsInterval and interval / interval_at. |
os-native |
✅ | Use the platform-native backend for OsInterval. No effect unless interval is also enabled. |
periodic |
Enables PeriodicInterval (Linux/BSD only). |
interval and periodic are independent. Disable interval if you only
need the cron-style PeriodicInterval; disable periodic (the default)
if you only need the full-featured OsInterval. Disabling os-native
forces OsInterval onto the portable tokio::time::sleep_until
fallback everywhere, which is useful for parity testing or for keeping
the timer entirely inside tokio.
PeriodicInterval
Behind the periodic feature flag, the crate also exposes
PeriodicInterval: a stripped-down ticker driven by a single,
kernel-side periodic timer.
use Duration;
use PeriodicInterval;
# async
Differences from OsInterval:
| Aspect | OsInterval |
PeriodicInterval |
|---|---|---|
| Kernel arming | One-shot, re-armed each tick | Periodic, armed once at construction |
MissedTickBehavior |
Burst / Delay / Skip | Always coalesces (Burst-equivalent) |
reset* methods |
✅ | ❌ |
| First-tick semantics | Fires immediately (or interval_at) |
Fires period after construction |
Returns from tick() |
Instant (scheduled deadline) |
io::Result<u64> (expiration count) |
| Platforms | Linux, *BSD, macOS, iOS, Windows, fallback | Linux, Android, *BSD, macOS, iOS |
| Per-tick syscalls | One re-arm per tick | Zero (just an fd read) |
It exists for the cron / heartbeat case where:
- you want the lowest possible per-tick overhead,
- coarse resolution is fine,
- coalescing (Burst) is the only behavior you need,
- you don't need
reset_*or phase control, - and you're OK with Linux-or-BSD-only.
For everything else, prefer OsInterval.
Caveats
OsIntervaldeliberately bypasses tokio's pauseable test clock. If your tests rely ontokio::time::pause(), either usetokio::time::Intervaldirectly or pull this crate in with the native backend disabled:
(= { = "1", = false, = ["interval"] }default-features = falsealone removesOsIntervalentirely — you must opt back in tointerval.)tick()isasyncand requires an active tokio runtime, just liketokio::time::interval.- The first
tick()returns immediately (matching tokio); useinterval_atto defer the first tick. reset*methods take effect on the nextpoll_tick/tickcall: the kernel timer is re-armed lazily.
MSRV
Rust 1.81
License
Licensed under the BSD 2-Clause License.
Copyright (c) 2026, Latigo LLC.