Why You Should Prefer Event Waits over Sleep(1) on Windows

· · Windows Development, Synchronization, Events, Timer, Design

In our previous post, A Practical Guide to Soft Real-Time on Windows, we discussed avoiding periodic loops that lean on Sleep. This time we focus on a single point from that discussion: why you should prefer an event wait over a short timer wait.

On Windows, a design that “checks back at fixed intervals” using Sleep(1) or waits with short timeouts is inevitably affected by the granularity of the system clock and the scheduling delay that follows. Under typical settings, a platform timer resolution on the order of 15.6ms is the usual baseline, so even if you intend to “look again in 1ms,” the wait you actually get is often quite coarse.

On the other hand, if what you really want to wait for is an “event” rather than “time” — work arriving, I/O completing, a stop request, a state change — there is no need to go check at fixed intervals. The side where the event occurs signals, and the waiting side waits on the event. That approach is more natural for latency, CPU, and power alike.

The questions this article aims to answer are these four:

  • Why are Sleep(1) and short timer waits less accurate than you might expect?
  • Why are event waits less subject to that limitation?
  • In which situations should you choose an event instead of a timer?
  • When should you still use a timer?

1. The Conclusion First

  • If you are waiting for work to arrive or I/O to complete, wait on an event, not a timer.
  • Timed waits on Windows are inevitably affected by the granularity of the system clock.
  • Sleep(1) does not mean “wake up exactly 1ms later.”
  • And even after the timeout elapses, the thread merely becomes ready first — immediate execution is not guaranteed.
  • That is why a design that “is really waiting for an event but goes to check with a timer” loses on both latency and power.
  • It is cleaner to reserve timers for cases where time itself is genuinely the condition.

Put in practical terms, it comes down to roughly this:

  • “Send metrics every 5 seconds” -> a job for a timer
  • “Run as soon as work lands in the queue” -> a job for an event / semaphore / condition variable / WaitOnAddress
  • “Continue once the I/O finishes” -> a job for a completion / event
  • “Stop when a stop request arrives” -> a job for a stop event / cancellation

2. What the Problem Is

2.1 Timed waits are bound by the system clock granularity

The timeout accuracy of the Windows wait functions depends on the system clock resolution. The same goes for Sleep: the milliseconds you specify are not guaranteed to be honored as “exactly that duration.”

The key point here is that specifying 1ms does not mean you will wake up 1ms later.

2.2 Even when the deadline arrives, execution is not necessarily immediate

What makes it even trickier is that the thread does not start running the moment the timeout elapses.

As the documentation for Sleep notes, once the wait interval ends the thread becomes ready, but there is no guarantee it gets the CPU and runs right away. It is affected by other threads, priority, CPU idle states, DPCs / ISRs, lock contention, and so on.

In other words, a short timer wait has at least two layers of uncertainty:

  1. The timeout determination itself is pulled along by the timer granularity
  2. Even after the timeout, when execution starts is up to the scheduler

2.3 Sleep(1) does not mean a 1ms period

When you see Sleep(1), it is easy to read it as “a loop that spins every 1ms.” But in reality, you must not read it that way.

while (!g_stop)
{
    Step();
    Sleep(1);
}

What this loop actually does is this:

  • The execution time of Step() is added every iteration
  • The wait time of Sleep(1) itself is pulled along by the granularity
  • Even after waking, the thread is not guaranteed to run immediately

3. Why Event Waits Win

3.1 The wait completes on a “signal,” not on “time running out”

Event waits are advantageous because they change the meaning of the wait.

A timer wait works like this:

  • Even if nothing has happened yet
  • You wake up when a fixed amount of time has passed
  • After waking, you check whether anything happened

An event wait works like this:

  • The side where something happened signals
  • When signaled, the wait is satisfied
  • At the moment you wake, there is already a reason
a point in timework arrivinga value changingI/O completiona stop requestwaiting threadwhat are you really waiting for?timer / waitable timerevent / semaphore / condition variableWaitOnAddresscompletion / eventstop event / cancellation

3.2 Pick the tool based on what you are waiting for

As a first cut, this table covers most decisions.

What you want to wait for Bad example First choice
Work landing in a queue TryPop with Sleep(1) event / semaphore
I/O completing Polling the status with a timer overlapped I/O event / IOCP
A stop request arriving Checking a stop flag every 100ms stop event / cancellation
A value changing within the same process while (flag == 0) Sleep(1) WaitOnAddress
A point in time arriving Forcing it onto an event timer / waitable timer

3.3 Events are not magic either

Event waits are advantageous in the sense that they do not need to wake on timer granularity, but they do not mean the thread runs with absolutely zero delay the instant it is signaled.

Even an event wait is still affected by:

  • scheduler latency
  • thread priority
  • CPU power states
  • lock contention
  • page faults
  • DPCs / ISRs

But at the very least, you eliminate the unnecessary kind of waiting where the thread is “asleep until the next timer tick.”

4. Typical Anti-Patterns

4.1 Polling a queue with Sleep(1)

This is the one you see most often.

for (;;)
{
    if (g_stop)
    {
        break;
    }

    WorkItem item;
    if (TryPop(item))
    {
        Process(item);
        continue;
    }

    Sleep(1);
}

This style looks simple at first glance, but it has three problems:

  1. The thread wakes up periodically even when the queue is empty
  2. Latency is pulled along by the timer granularity
  3. It also loses on power

4.2 Watching state with Thread.Sleep(1) / Task.Delay(1)

The same smell shows up in C# / .NET as well.

while (!stoppingToken.IsCancellationRequested)
{
    if (_queue.TryDequeue(out WorkItem? item))
    {
        await ProcessAsync(item, stoppingToken);
        continue;
    }

    await Task.Delay(1, stoppingToken);
}

It may look gentle and async on the surface, but the essence of the design is still polling.

5. How to Fix It

5.1 The producer signals on arrival

If you are waiting for queue arrivals, change the design so that the producer signals instead of polling.

  • The producer puts an item into the queue
  • Immediately after enqueueing, it calls SetEvent
  • The consumer waits with WaitForSingleObject or WaitForMultipleObjects
  • When it wakes, it drains the queue

5.2 Waiting on work and stop together with WaitForMultipleObjects

For a simple worker, this shape is easy to follow.

HANDLE waits[2] = { _stopEvent, _workEvent };

for (;;)
{
    DWORD rc = WaitForMultipleObjects(2, waits, FALSE, INFINITE);

    if (rc == WAIT_OBJECT_0)
    {
        return;
    }

    if (rc != WAIT_OBJECT_0 + 1)
    {
        throw std::runtime_error("WaitForMultipleObjects failed.");
    }

    DrainQueue();
}

The key points in this example are three:

  • Sleep(1) is gone
  • The producer calls SetEvent when an item arrives
  • The worker waits on stop and work at the same time

5.3 Within a single process, WaitOnAddress is also a candidate

If all you want is to “wait until some value changes” within the same process, WaitOnAddress is also a strong option.

As a rough guide for choosing:

  • Cross-process or general wait targets -> event / semaphore / waitable object
  • Lightweight value changes within the same process -> WaitOnAddress

6. When You Still Use a Timer

6.1 When time itself is the condition

Of course, there are legitimate uses for timers.

  • Sending metrics every 5 seconds
  • Retrying after 200ms
  • Sweeping a cache every minute
  • Waiting until a deadline and treating it as a timeout

In these cases, what you want to wait for really is time.

6.2 Use a waitable timer

If you are waiting for “time itself” on Windows, using a waitable timer makes the intent clearer than carelessly stacking up Sleep calls.

6.3 Do not make timeBeginPeriod a habit

When the accuracy of short timer waits starts to bother you, it is tempting to throw in timeBeginPeriod(1). But this should not be your default first choice.

There are three reasons:

  1. It has a power / performance cost
  2. On recent versions of Windows, the behavior is a bit more complicated
  3. It often means you have not fixed the root cause

7. Review Checklist

  • Are you building a check-back loop with Sleep(1) / Thread.Sleep(1) / Task.Delay(1)?
  • Are you timer-polling when what you are really waiting for is a queue arrival, I/O completion, or a stop request?
  • Is the design such that the producer / completion side can signal?
  • Can stop and work be waited on together in a single wait?
  • For value changes within the same process, can it be written with WaitOnAddress?
  • Wherever a timer is used, is what you actually want to wait for really “time”?

8. Summary

On Windows, a design that uses short timer waits to “check back at fixed intervals” is inevitably affected by timer granularity and the scheduler. As a result, Sleep(1) and short timeouts are not as precise a wait as they appear.

On the other hand, if what you really want to wait for is an “event” — work arriving, I/O completing, a stop request, a state change — an event wait is the more natural fit.

It all boils down to this one line:

Wait on a timer for time; wait on an event for events.

Just having this boundary clearly drawn pays off in several ways:

  • Latency becomes easier to reason about
  • Unnecessary periodic wakeups go down
  • Power consumption improves
  • The intent of the code becomes easier to read

9. References

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Windows App Development

Replacing timer polling with event-driven design in Windows applications and services directly affects implementation quality, making it a core Windows app development theme.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog