Why You Should Prefer Event Waits over Sleep(1) on Windows
· Go Komura · 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:
- The timeout determination itself is pulled along by the timer granularity
- 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
flowchart LR
start["waiting thread"] --> q{"what are you really waiting for?"}
q -- "a point in time" --> timer["timer / waitable timer"]
q -- "work arriving" --> event["event / semaphore / condition variable"]
q -- "a value changing" --> addr["WaitOnAddress"]
q -- "I/O completion" --> io["completion / event"]
q -- "a stop request" --> stop["stop 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:
- The thread wakes up periodically even when the queue is empty
- Latency is pulled along by the timer granularity
- 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
WaitForSingleObjectorWaitForMultipleObjects - 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
SetEventwhen an item arrives - The worker waits on
stopandworkat 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:
- It has a power / performance cost
- On recent versions of Windows, the behavior is a bit more complicated
- 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
stopandworkbe 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
- Sleep function (Win32)
- Wait Functions
- WaitForSingleObject function
- Event Objects (Synchronization)
- Using Event Objects
- WaitOnAddress function
- WakeByAddressSingle function
- timeBeginPeriod function
- CreateWaitableTimerExW function
- SetWaitableTimer function
- Thread.Sleep Method (.NET)
- Results for the Idle Energy Efficiency Assessment
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
A Decision Table for Whether to Exit or Continue After an Unexpected Exception
When an unexpected exception occurs, should the app exit or keep running? We organize the decision from the perspectives of state corrupt...
A Minimum Security Checklist for Windows App Development
A checklist-style guide to the security basics for WPF / WinForms / WinUI / C++ / C# business apps: privileges, signing, updates, secrets...
Choosing Between .NET's Three Timers - PeriodicTimer/Timer/DispatcherTimer
The differences between PeriodicTimer / System.Threading.Timer / DispatcherTimer, and how to choose between them for async processing, Th...
Why Use the .NET Generic Host and BackgroundService in Desktop Apps
How to use the Generic Host and BackgroundService to organize startup, periodic processing, shutdown, logging, configuration, and DI in W...
A Practical Guide to FileSystemWatcher - Handling Missed and Duplicate Events
We organize how to use FileSystemWatcher and its pitfalls - missed events, duplicate notifications, completion-detection traps, rescans, ...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
UI Threading & Timers
Topic page for WPF / WinForms UI threading, async flow, Dispatcher usage, and timer decisions.
Where This Topic Connects
This article connects naturally to the following service pages.
Technical Consulting & Design Review
This topic covers wait design, choosing synchronization primitives, and the latency/power trade-offs in soft real-time systems, so it pairs well with technical consulting and design reviews.
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.
Public links