Skip to content

TimerWheel

Eugene Lazutkin edited this page Jul 18, 2026 · 1 revision

Hashed timing wheel (Varghese & Lauck) — O(1) schedule, cancel, and reschedule with O(1) amortized cost per timer per tick. The wheel keeps logical time: the caller drives the clock by calling tick()/advance() from its own time source (an interval, a game loop, a simulation), which keeps the structure runtime-agnostic and fully testable.

See also: IndexedHeap / MinHeap — heap-based scheduling with exact arbitrary priorities and no tick driving; Choosing a data structure for the trade-off discussion.

Legend for tables
  • API
    • options - an object with optional properties:
      • slots - the number of wheel slots. Default: 256 (rounded up to a power of two). More slots mean fewer multi-round timers per revolution.
    • value - the payload to be scheduled
    • entry - a timer handle returned by schedule()
    • delay - ticks until due (clamped to ≥ 1 — the soonest a timer can fire is the next tick)
  • Complexity
    • O - complexity of an operation
    • O(1) - constant time
    • O(n) - linear time proportional to the number of pending timers
    • O(s) - linear time proportional to the number of slots

Implementation of TimerWheel.

import TimerWheel from 'list-toolkit/timer-wheel.js';

const wheel = new TimerWheel({slots: 1024});

const handle = wheel.schedule(task, 150); // due in 150 ticks — O(1)
wheel.reschedule(handle, 42); //             move it — O(1)
wheel.cancel(handle); //                     cancel — O(1)

// drive the clock from any time source:
setInterval(() => {
  for (const task of wheel.tick()) task.run();
}, 10); // 1 tick = 10ms of resolution

Class TimerWheel

Methods:

Member Return type Description O
constructor(options) this constructs a new TimerWheel O(s)
size number the number of pending timers O(1)
isEmpty boolean checks if no timers are pending O(1)
position number the current slot position O(1)
currentTick number ticks processed since construction O(1)
schedule(value, delay) entry schedule a payload; returns the timer handle O(1)
isScheduled(entry) boolean checks if the handle is currently pending O(1)
cancel(entry) boolean cancel a pending timer; true if it was pending O(1)
reschedule(entry, delay) entry move a timer to a new delay; also revives a fired/cancelled one O(1)
remainingTicks(entry) number | undefined ticks left until the handle fires O(1)
tick() array of values advance one tick; return payloads that came due amortized
advance(ticks) array of values advance several ticks; return due payloads in firing order amortized
clear() this cancel everything (handles become unscheduled) O(n + s)
[Symbol.iterator]() iterator iterate pending payloads in wheel-slot order (not time order) O(1)

Notes on properties and methods

Logical time. The wheel never touches setTimeout or Date — a tick means whatever the caller wants (10ms, one frame, one simulation step). This is what makes it portable across runtimes and trivially testable; it also means delays quantize to whole ticks — pick the slot count and tick duration to match the needed resolution and horizon (delays longer than slots are handled with revolution counting).

Handles: schedule() returns the entry — keep it for cancel/reschedule/remainingTicks. isScheduled() is honest (slot = -1 marks fired/cancelled entries), and reschedule() on a fired or cancelled handle revives it. Handles are owned by the wheel that created them; the entry's bookkeeping properties (slot, rounds, next, prev) belong to the wheel. Same-tick timers fire in scheduling order.

Wheel vs heap (measured on bench/bench-timers.js, 2026-07-17, 10k timers, horizon 1k ticks): schedule + drain everything — TimerWheel 402μs vs IndexedHeap 2.12ms (~5×); 100k reschedules — 7.0ms vs 7.4ms (parity). The wheel wins on bulk expiry and O(1) operations; the heap gives exact arbitrary priorities with no tick driving and no resolution quantization. Internally each slot is a node-based List using the raw splice/pop primitives — zero allocation per operation.

Exports

TimerWheel is the default export and is also exported by name.

Clone this wiki locally