Skip to content
WindowsDeep Dive Published Updated 6 min readViews unavailable

Windows Waitable Timers: Deadlines, Periodicity, and Safe Cancellation

Use waitable timers as synchronization objects, interpret relative due times correctly, and handle periodic signals, cancellation races, and power costs.

A Windows waitable timer is a kernel synchronization object that becomes signaled when its due time arrives. A thread can wait on it alongside events or other waitable objects, so a timer can represent a deadline in the same control flow as I/O completion or shutdown. This differs from a sleep call: the timer has a handle, can be named and opened by another process when configured that way, and can participate in the ordinary Windows wait APIs.

Correct timer use requires understanding its reset mode, due-time units, and cancellation semantics. A timer signal is not a guarantee that the application will run at an exact wall-clock instant; the scheduler must still dispatch the waiting thread. Periodic timers are also not free background work: frequent wakeups prevent the processor from remaining in low-power states. Choose a one-shot deadline when possible, wait on application events between deadlines, and treat cancellation as a state change that must be synchronized with the consumer.

Manual-reset and synchronization timers

Create a waitable timer with CreateWaitableTimerW or CreateWaitableTimerExW. A manual-reset timer remains signaled until a new due time is established with SetWaitableTimer; every waiter can observe that signaled state. A synchronization timer is consumed by a successful wait, returning it to the nonsignaled state. This is similar to the difference between manual-reset and auto-reset events, but the timer itself supplies the expiration signal.

Pick the type based on how many waiters should observe an expiration. If a single worker owns the timer, a synchronization timer can naturally release one waiter. A manual-reset timer is useful when multiple observers must see that a deadline passed, but every observer must understand when the shared signal is reset. For many independent deadlines, use separate timer objects or a scheduler abstraction rather than sharing one handle without a clearly defined owner.

Relative due times use 100-nanosecond units

SetWaitableTimer accepts a LARGE_INTEGER due time. A negative value specifies a relative interval measured in 100-nanosecond units; -10,000,000 therefore represents one second. A positive value represents an absolute time. Use the relative form for elapsed-time deadlines so that a wall-clock adjustment does not change what “wait one second from now” means.

HANDLE timer = CreateWaitableTimerW(nullptr, TRUE, nullptr);
if (timer == nullptr) {
    return GetLastError();
}

LARGE_INTEGER due{};
due.QuadPart = -10LL * 1000LL * 1000LL; // One second, in 100-ns units.

if (!SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE)) {
    const DWORD error = GetLastError();
    CloseHandle(timer);
    return error;
}

const DWORD wait = WaitForSingleObject(timer, INFINITE);
// WAIT_OBJECT_0 means the timer became signaled.
// Handle WAIT_FAILED and application shutdown according to the caller's policy.
CloseHandle(timer);

The example creates a manual-reset one-shot timer: bManualReset is TRUE, the period is zero, and no completion routine is provided. Real code should use a wait that also includes its shutdown event when the worker must be stoppable, typically WaitForMultipleObjects, rather than forcing teardown to wait for an arbitrarily distant deadline. Every successful timer creation needs an owner and a matching CloseHandle after no thread can still wait on it.

An absolute system-time deadline has different semantics from a relative interval. It is tied to system time and can be affected by time changes according to the timer API’s documented rules. Do not store a monotonic application deadline as though it were a calendar timestamp. If an operation has a deadline expressed in wall-clock time, convert and validate it deliberately; if it has a duration, keep it relative and account for the time already spent in earlier work.

Periodic timers and missed work

The lPeriod parameter of SetWaitableTimer is in milliseconds. A zero period makes the timer one-shot. A nonzero period reactivates it after each interval until it is reset or canceled. Do not assume a periodic signal is a durable queue of every missed tick: a timer object represents signaled state, not an unbounded counter of how many intervals passed while no thread was waiting.

For polling jobs, decide whether the schedule is fixed-rate or fixed-delay. A fixed-rate task targets periodic instants and may need to skip missed intervals when the worker falls behind. A fixed-delay task schedules the next run only after the current work completes. These policies are application-level; simply configuring a periodic timer does not make the callback workload safe from overlap, backpressure, or drift. If work must never overlap, use one owner thread or protect execution with an explicit state transition.

High-frequency timers can have system-wide power consequences because each signal requires processor activity. Prefer event-driven completion, notification objects, or a coarser schedule when the product requirements allow it. Do not reduce timer resolution globally merely to make one application appear more responsive; measure end-to-end latency and power use on representative hardware.

Cancellation is not completion

CancelWaitableTimer makes an active timer inactive; it does not close the handle. A timer can already have become signaled when cancellation occurs, and a waiter may have consumed the signal. Coordinate the decision to cancel, the wait result, and the lifetime of the object under one owner. Closing the handle while another thread is still waiting or about to issue a wait creates an ownership race, not a cancellation protocol.

If using a completion routine with SetWaitableTimer, remember that it is delivered as an APC to the thread that set the timer and runs only when that thread enters an alertable wait. Canceling the timer does not turn a normal non-alertable wait into an APC dispatch point. For callback-based background work, the Windows thread-pool timer APIs are generally a better fit because the pool owns callback scheduling and exposes callback-drain operations.

Waiting with other state

Production workers commonly wait on both a timer and a stop event. When multiple handles become signaled, the result from WaitForMultipleObjects identifies which condition woke the thread; if the wait-any call sees more than one signaled handle, selection follows the API’s documented handle order. Structure the array and switch accordingly, and consider whether the worker should process a final timer tick when stop is also signaled.

If a timer is reused, resetting it changes the due time and returns its state to nonsignaled as documented. A thread that has already returned from a wait may still be processing the previous deadline when another thread resets the timer. Serialize reconfiguration with the consumer or attach a generation number to work so stale wakeups cannot act on a new schedule. Keep timer policy separate from job state: the timer says when to reconsider the job, while the application state says whether the job is still valid.

Diagnostics and tests

Measure observed lateness as the difference between the intended due time and the time the worker begins useful work. That includes timer behavior, runnable-thread scheduling, CPU pressure, lock contention, and time spent in the wait loop. It is usually more informative than comparing API call durations. Test under idle and CPU-loaded conditions, with shutdown racing expiration, repeated timer resets, and a long-running worker that misses several periods.

Log timer creation failures, set/reset failures, wait results, cancellation requests, and final close ownership. Avoid logging every high-frequency tick in production; that can create the very load and wakeups being investigated. Waitable timers are useful, precise control-flow tools, but they are not hard real-time guarantees. Treat them as one signal in a scheduler, preserve clear ownership, and define what late, repeated, canceled, and simultaneous events mean to the application.

Related:

Sources:

Comments