A Practical Guide to Getting as Close to Soft Real-Time as Possible on Ordinary Windows

· Updated: · · Windows Development, Soft Real-Time, Design, Measurement

Revision history (1 updates, last updated Sep 1, 2026)

A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.

Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614459)
First published
Cite this article(DOI: 10.5281/zenodo.21614458)

This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.

Go Komura (2026). A Practical Guide to Getting as Close to Soft Real-Time as Possible on Ordinary Windows. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614458 https://comcomponent.com/en/blog/2026/03/09/000-windows-soft-realtime-practical-guide-natural/

DOI (latest version)
10.5281/zenodo.21614458
DOI (this version)
10.5281/zenodo.22217121

When you build processing on Windows where being late is a problem - periodic processing, audio, video, measurement, equipment control - the impression that Windows is not really up to it tends to come up. That impression is half right and half wrong: Windows is not a hard real-time OS, but if you properly nail down design, implementation, measurement, and operations, you can get it to a genuinely practical state as soft real-time.

What this article covers is ordinary Windows 10 / 11, without special RTOS extensions, custom kernel drivers, or dedicated controllers. It is a practice-oriented discussion of how far you can push down latency and jitter with a user-mode app on an everyday desktop or laptop PC. Audio, video, periodic control, and data acquisition differ in their details, but the trouble spots are largely shared, so this time we have collected that common ground in the form of a checklist.

Who This Is For, and the Languages Used in the Code Examples

This is written for developers building processing on Windows where being late is a problem: periodic control, audio and video, measurement, and equipment control. The assumption is user-mode application development; implementing kernel-mode drivers is out of scope.

The code examples split across languages as follows.

Content Language Where
Periodic loops, MMCSS, power QoS, and other places that call Win32 APIs directly C++ (Win32) 4.1, 4.3, 4.5
How to call the same Win32 APIs from C# C# (P/Invoke) 4.5
Time measurement, GC, and allocation caveats .NET (C#) The .NET-side checks in 4.4, and 5.2

The discussion of causes up through chapter 3, and the checklist itself in chapter 4, do not depend on the language. If you work only in C#, reading the C++ code as an explanation of which APIs to call in which order is enough.

Table of Contents

  1. The Conclusion First (In One Line)
    • 1.1. Quick Reference by Period Range (Where to Start Reading)
  2. What “Soft Real-Time” Means on Ordinary Windows
    • 2.1. What This Article Means by “Ordinary Windows”
    • 2.2. What Is Achievable, and Where It Gets Hard
    • 2.3. A Quick Word on Terminology
  3. The Main Causes of Latency and Jitter
    • 3.1. The Scheduler and Priorities
    • 3.2. DPCs / ISRs and Drivers
    • 3.3. Page Faults and Memory
    • 3.4. Timer Resolution and Power Management
    • 3.5. Core Migration and Heat
  4. A Practical Checklist for Reducing Lateness on Ordinary Windows
    • 4.1. Periodic Loops and How to Wait
    • 4.2. Fast Path / Slow Path and Fixed-Length Queues
    • 4.3. Priorities / MMCSS / Background Mode
    • 4.4. Memory / GC / First-Run Costs
    • 4.5. Power Settings / EcoQoS / Timer Resolution
    • 4.6. CPU Placement / Core Migration / Heat
    • 4.7. Isolating Drivers / DPCs / ISRs / External Disturbances
  5. Measurement and Evaluation
    • 5.1. What to Record
    • 5.2. How to Read p99 / p99.9 / max
    • 5.3. What to Measure With
    • 5.4. Testing Discipline
  6. A Rough Guide to Choosing
  7. Conclusion
  8. References

Knowledge map for this article

Aiming for soft real-time on ordinary Windows means not asking for the hard real-time that guarantees zero deadline misses, but instead keeping latency and jitter small and designing the system so that nothing breaks when a deadline is missed. Periodic waiting rests on measurement with QueryPerformanceCounter and a high-precision waitable timer, and timer resolution is raised with timeBeginPeriod only for as long as it is actually needed. Unless the demotion to EcoQoS and the disregard for timer resolution that Power Throttling brings are explicitly turned off, they become one more cause of growing jitter alongside DPC/ISR, page faults, and thermal throttling. For continuous processing such as audio and video, MMCSS prevents deadline overruns by giving the work preferential CPU allocation, CPU placement should start with a loose specification such as CPU Sets before the stronger option of pinning to specific CPUs, and WPR/WPA and LatencyMon are the tools for isolating the cause.

Soft real-time on ordinary WindowsDiagram showing the waitable timers and MMCSS that underpin soft real-time, the relationship between Power Throttling and EcoQoS, the paths by which DPC/ISR and page faults create jitter, the progression from CPU Sets to hard CPU pinning, and how WPR/WPA and LatencyMon relate as measurement tools.usesusesusesincompatible withusespreventsmay causeusesmitigatesmitigatesmay causenot recommended forusesshould come beforemay causemay causemay causeverified byverified byverified bySoft Real-TimeQPC (QueryPerformanceCounter)Waitable TimertimeBeginPeriod (Timer Resolution Request)Power ThrottlingEcoQoSJitterMMCSS (Multimedia Class Scheduler Service)Deadline missThread Priority / Priority ClassCPU SetsCPU affinityThermal ThrottlingDPC / ISRPage FaultWPR / WPA ToolsetLatencyMon

In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (20 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle

1. The Conclusion First (In One Line)

  • What you aim for on ordinary Windows is not a hard real-time guarantee, but a soft real-time configuration that is unlikely to be late and does not break when it is.
  • The biggest single win is making the hot path short, fixed-length, and non-blocking.
  • Separate the fast path (acquisition / control) from the slow path (storage / communication / UI) and connect them with a fixed-length queue.
  • Drive the periodic loop on absolute deadlines, not by leaning on Sleep(1).
  • For continuous streams like audio and video, consider MMCSS first.
  • For time measurement, use QueryPerformanceCounter (QPC) - in .NET, Stopwatch.
  • For waiting, prefer device events or high-resolution waitable timers.
  • Use timeBeginPeriod only for as long as needed. Do not design on the assumption that it is always on.
  • In real operation, AC power, the power mode, how EcoQoS is handled, and pruning background load all pay off.
  • Evaluate not just averages, but p99 (the threshold where the slowest 1 in 100 starts to show) / p99.9 / max / miss count / DPC / ISR / page faults / queue depth.

In short, on ordinary Windows, reducing the reasons for lateness through design beats raising priorities. Priorities and power settings matter, but they alone cannot create stability.

Design beats priority tuning at creating stabilityShows the conclusion that making the hot path short and fixed-length and non-blocking so design removes the reasons for lateness has the biggest effect, and that priority and power settings matter but cannot create stability on their own.Keep the hot path short and fixed-lengthDesign removes the reasons for latenessRarely late, and unbroken when it isTuning priorities and power settingsImportant but not enough on its own

Figure 1: The biggest win is not priority tuning but removing the reasons for lateness in the design itself.

1.1. Quick Reference by Period Range (Where to Start Reading)

This is a long article, so the quick reference comes first, so that you can read only the row that matches your case. It used to sit at the end, in chapter 6, and has been moved up to here.

Period / requirement The setup to build first Sections to focus on
10-20 ms class, where occasional wobble can be absorbed Fast path / slow path separation, a fixed-length queue, normal to slightly elevated priority, event-driven design. This is often enough 4.1, 4.2
1-5 ms class, where you have to keep up continuously All of the above, plus an allocation-free hot path, a dedicated thread, MMCSS or careful priority tuning, a high-resolution waitable timer, AC power, and a review of the power settings 4.1 - 4.5
Approaching sub-1 ms, and you cannot miss even over long runs under heavy load Very hard with user-mode alone on ordinary Windows. Consider first a design that moves the critical part somewhere else: device-side firmware, a dedicated controller, an FPGA, an RTOS 2.2, 6
GUI / logging / communication / DB all living together Do not cram it all into one process and one loop; separate the responsibilities. Downstream concerns easily break upstream deadlines 4.2, 4.3, 6

Isolating causes and measurement discipline are the same across every range (chapters 3 and 5).

2. What “Soft Real-Time” Means on Ordinary Windows

2.1. What This Article Means by “Ordinary Windows”

By ordinary Windows we roughly assume the following.

  • A typical Windows 10 / 11 desktop or laptop PC
  • No custom RTOS extensions
  • No custom kernel-mode driver development
  • A normal user-mode app
  • Tuning with standard Windows APIs and settings

In other words, this is not about building out a whole dedicated machine for real-time control - it is about how far you can realistically push things on an ordinary Windows PC.

Ordinary Windows 10 / 11 PCUser-mode appAim for soft real-timeKeep latency lowKeep jitter smallObserve deadline misses and avoid breakingNeed to guarantee zero deadline violationsRTOS / dedicated controller / FPGA / device-side processing

Figure 2: What a user-mode app on ordinary Windows aims for is soft real-time; guaranteeing zero deadline violations belongs to RTOSes and similar territory.

2.2. What Is Achievable, and Where It Gets Hard

Even on ordinary Windows, you can build a genuinely rarely-late setup for processing like the following.

  • Periodic processing from a few milliseconds to tens of milliseconds
  • Buffer-driven audio / video
  • Sensor acquisition and control loops
  • Soft-PLC-style fixed-period processing
  • A low-latency pipeline running on a thread separate from the UI

That said, achievable here does not mean the occasional latency spike can be reduced to absolute zero. The state we aim for is this.

  • Keep normal-case latency low
  • Keep jitter small
  • Do not break when a deadline is occasionally missed
  • Be able to observe the fact that it was missed

Conversely, requirements like the following become very hard to satisfy with user-mode alone on ordinary Windows.

  • Guaranteeing zero deadline violations
  • Holding under a few hundred microseconds stably over long periods
  • Coexisting with a heavy GUI, network, and storage
  • Doing it on battery power or with power-saving priorities intact
  • Not tolerating even spikes caused by drivers or devices

For these, it is safer to also consider moving only the truly time-critical part to device-side firmware, a dedicated controller, an FPGA, or an RTOS.

What soft real-time aims for and when to move work outShows that on ordinary Windows the aim is to keep normal-case latency low and jitter small and to survive an occasional missed deadline while still observing it, and that a requirement of zero deadline violations means moving only the time-critical part elsewhere.What soft real-time aims forLow normal-case latencySmall jitterSurvives a miss and can observe itRequirement of zero deadline violationsMove it to the device or an RTOS

Figure 3: What achievable means here is not zero spikes, but a system that is rarely late, does not break, and can observe when it is.

2.3. A Quick Word on Terminology

Let us pin down the terms used in this article first.

Term In one line Practical view
soft real-time Occasional lateness is possible; the approach is to keep it small and to survive it This is what to aim for first on ordinary Windows
hard real-time The world where zero deadline violations must be guaranteed Not a target for user-mode alone on ordinary Windows
Jitter Variation in period or response time Even with a good average, large jitter means instability in real operation
deadline miss Processing not finishing by its scheduled time Do not hide it - count it and log it
p99 / p99.9 Metrics for looking at the slow tail p99 is the threshold where the slowest 1 in 100 starts to show
DPC / ISR Kernel-side processing around drivers and interrupts When long, user-mode threads are made to wait
MMCSS The Windows mechanism that allocates CPU to time-sensitive work like audio / video A strong option for processing that must never starve its buffers
QPC QueryPerformanceCounter The basis of elapsed-time measurement. A high-resolution counter, not the wall clock
waitable timer A kernel object that becomes signaled at a specified time. Passing CREATE_WAITABLE_TIMER_HIGH_RESOLUTION to CreateWaitableTimerExW gives you the high-resolution version A better foundation for periodic waiting than Sleep (4.1)
EcoQoS A classification meaning power efficiency may take priority. The CPU frequency may be lowered, or the work moved onto efficiency cores Avoid it for time-sensitive work. If you do not state your intent, the OS infers it automatically (4.5)
IGNORE_TIMER_RESOLUTION A setting that says the process’s timer-resolution request may be ignored (PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION) While it is in effect, timeBeginPeriod has no effect. On Windows 11 it can be applied automatically once the process is hidden from the screen (4.5)
CPU Sets A mechanism for asking softly that a thread or process run on a given set of cores Try it before pinning to specific cores (4.6)
ETW / WPR / WPA The standard Windows tracing infrastructure (ETW) plus its recording tool (WPR) and analysis GUI (WPA) Use them to dig into context switches, DPC / ISR, and page faults (5.3)
LatencyMon A third-party tool for looking at driver-induced latency Scan DPC / ISR execution times per driver to get a bearing (5.3)

3. The Main Causes of Latency and Jitter

The reasons periodic processing falls behind on ordinary Windows almost always trace back to one of the boxes in this diagram.

Periodic processing falls behindScheduler / prioritiesDPC / ISR / driversPage faults / memoryTimer resolution / power managementCore migration / heat

Figure 4: The reasons periodic processing falls behind trace back to five families: the scheduler, DPC/ISR, memory, timers and power, and core migration and heat.

3.1. The Scheduler and Priorities

Windows threads run in priority order. At equal priority they take turns round-robin, and when a higher-priority thread becomes runnable, lower-priority threads get pushed aside.

So even if you write your periodic thread diligently, it is entirely normal for the following to run first:

  • Other threads
  • Other processes
  • OS-internal work
  • Security products
  • Device helper processing
  • Background synchronization
Preemption under priority schedulingShows that threads at the same priority take turns round-robin and that when a higher-priority thread becomes runnable the lower-priority ones are pushed aside, so it is normal for another process or OS-internal work to run ahead of the periodic thread.Threads at the same priorityTake turns round-robinA higher-priority thread becomes runnableLower-priority threads are pushed asideAnother process, OS-internal work, and so on

Figure 5: However diligently you write the periodic thread, higher-priority work running first is entirely normal.

3.2. DPCs / ISRs and Drivers

This part is quite important. Even with your app-side priorities in order, if DPCs (Deferred Procedure Calls) or ISRs (Interrupt Service Routines) run long, user-mode threads cannot execute during that time.

The devices and drivers that commonly cause this include:

  • USB
  • Wi-Fi / Bluetooth
  • Storage
  • Audio
  • GPU
  • ACPI / power management

Even when your application code is fine, you can get stalled by driver or hardware circumstances. Thinking you will just raise your app’s priority higher and win here usually ends in pain.

How DPCs and ISRs stall user modeShows that when the ISRs and DPCs of devices and drivers such as USB and Wi-Fi and the GPU run long, user-mode threads cannot execute during that time, and that raising the application priority does not win against them.Devices such as USB, Wi-Fi, and the GPUISRs and DPCs run on the kernel sideUser mode cannot execute during that timeRaising the priority does not win

Figure 6: Even when your application code is fine, driver and hardware circumstances can stall user mode.

3.3. Page Faults and Memory

If a page fault (a needed page not being in memory and having to be fetched) occurs on the hot path, latency balloons instantly.

Patterns particularly worth avoiding:

  • Page commit on first access
  • Lazy loading
  • Page-in of memory-mapped files
  • More dynamic allocation than necessary
  • Large objects or a fragmented heap

For the body of periodic processing, the right posture is roughly: allocate the memory you need up front, and touch it once at startup.

Latency from a page fault on the hot pathShows that if a page touched on the hot path is not in memory a page fault has to go fetch it and latency balloons, so allocating the memory you need up front and touching it once at startup is the right move.YesNoTouch memory on the hot pathIs the page in memory?Continue as-isA page fault fetches itLatency balloonsAllocate up front and touch once at startup

Figure 7: Whether a page fault occurs is what makes latency jump. The countermeasures are preallocation and a startup warm-up.

3.4. Timer Resolution and Power Management

“I want to run every 1 ms, so Sleep(1)” almost never works out. Windows wait precision is affected by timer resolution, scheduling, and power state.

Furthermore, do not overlook that raising the timer resolution slightly improves wait precision but has side effects on power consumption and overall system behavior.

3.5. Core Migration and Heat

When a thread migrates between cores, the caches have to warm up again. The OS often handles this well by itself, but under heavy load it becomes a source of wobble.

Heat also becomes non-negligible over long runs. When thermal throttling kicks in, a previously stable period can fall apart.

How core migration and heat break the periodShows that a thread migrating between cores forces the caches to warm up again which becomes a source of wobble under heavy load, and that heat building up over a long run brings thermal throttling that breaks a previously stable period.Thread migrates between coresCaches have to warm up againA source of wobble under heavy loadHeat builds up over a long runThermal throttlingA previously stable period falls apart

Figure 8: Core migration eats away at long-run stability as jitter, and heat does the same as throttling.

4. A Practical Checklist for Reducing Lateness on Ordinary Windows

Here begins the practical part. For the causes seen in the previous section, we summarize what to check, what to avoid, and what to decide first on ordinary Windows, in checklist form.

4.1. Periodic Loops and How to Wait

First, the classic anti-pattern is this.

while (running)
{
    Sleep(1);
    Step();
}

This is not a 1 ms period - it is a loop that waits roughly 1 ms or more, then adds the execution time of Step() on top. Worse, the wait overshoot accumulates directly.

Absolute-deadline basedRelative-time basedWaitUntil(next - margin)next += periodShort spin if neededFastStep()Step()Sleep(1)Wait error and execution time pile up bit by bitResists accumulating drift

Figure 9: A relative-time loop that leans on Sleep(1) piles up error, while a loop driven by absolute deadlines resists accumulating drift.

Checklist

  • The periodic loop is not built on Sleep(1)
  • The period is driven by absolute deadlines via next += period
  • Waiting prefers device events or waitable timers
  • Only the final fine adjustment uses a very short busy-spin
  • timeBeginPeriod is used only while needed and reverted afterwards
  • Behavior has been verified while minimized / hidden / not visible

A periodic loop is more stable when driven by absolute deadlines rather than relative time.

int64_t next = QpcNow() + periodTicks;

while (running)
{
    WaitUntil(next - wakeMarginTicks);

    while (QpcNow() < next)
    {
        CpuRelax(); // Spin briefly only at the very end
    }

    int64_t started = QpcNow();
    FastStep();
    int64_t finished = QpcNow();

    RecordTiming(next, started, finished);

    next += periodTicks;

    while (finished > next)
    {
        ++missedDeadlines;
        next += periodTicks;
    }
}

4.2. Fast Path / Slow Path and Fixed-Length Queues

The architectural basis is to put only deadline-sensitive work on the fast path, and push everything else to the slow path.

Device / acquisition eventsfast path: acquire, control, minimal copyingFixed-length queueslow path: store, send, UI, aggregationRecord lateness / misses / queue depth

Figure 10: The basic layout: only deadline-sensitive work on the fast path, pushed out to the slow path through a fixed-length queue.

Limit the fast path to roughly this much.

  • Data acquisition
  • Control-value computation
  • The minimum necessary copying
  • Timestamping
  • Enqueueing
  • Recording misses / overruns

Everything else drops to the slow path.

Checklist

  • No file writes, network sends, or DB writes on the hot path
  • No heavy logging, Flush, or synchronous RPC on the hot path
  • Fast path / slow path clearly separated by thread or responsibility
  • The queue is fixed-length
  • The policy for queue overflow is decided in advance
  • Miss counts, drop counts, and queue depth are being observed
  • UI updates and log aggregation are separated to a lower frequency

When the queue fills up, it is safer not to leave the policy vague.

Latest value mattersEvery record mattersLogging useQueue is fullWhat do we protect?Drop old entries, keep the latestAlert / stop / upstream throttlingDrop old entries, record only the drop count

Figure 11: What you protect when the queue fills up is a decision to make in advance, and it differs by use case.

4.3. Priorities / MMCSS / Background Mode

The basic rule of priorities is: do not raise everything. On ordinary Windows, raising only the important threads and properly lowering the housekeeping work works better. Background mode is a mechanism that treats not just CPU but also resources like I/O at lower priority.

Split the workDeadline-sensitive threadsStore / send / compress / aggregateUIHigher priority or MMCSS if neededBackground mode / lower priorityNormal priorityDo not start with REALTIME_PRIORITY_CLASS

Figure 12: How to assign priorities: raise only the deadline-sensitive threads, and lower the housekeeping work properly.

Checklist

  • Not all threads are set to high priority
  • Only genuinely time-critical threads are raised
  • Housekeeping work like storing, sending, compressing, and syncing is dropped to background mode
  • MMCSS is considered for continuous buffer processing such as audio, video, capture, and playback
  • Thinking per-thread first, before the whole process
  • REALTIME_PRIORITY_CLASS is not used until the need is clearly established

MMCSS (Multimedia Class Scheduler Service) is especially effective for processing that has to fill a buffer within a fixed time, like audio / video. It aligns with Windows’ design better than simply spinning a high-priority thread at all times.

The code looks roughly like this.

DWORD taskIndex = 0;
HANDLE avrt = AvSetMmThreadCharacteristicsW(L"Pro Audio", &taskIndex);
if (!avrt)
{
    throw std::runtime_error("AvSetMmThreadCharacteristicsW failed");
}

// Run the time-sensitive loop

if (!AvRevertMmThreadCharacteristics(avrt))
{
    throw std::runtime_error("AvRevertMmThreadCharacteristics failed");
}
Registering and reverting a thread that uses MMCSSShows the flow of registering with MMCSS through AvSetMmThreadCharacteristicsW before the time-sensitive loop and reverting with AvRevertMmThreadCharacteristics once the loop is done, and that MMCSS gives the registered thread preferential CPU allocation.AvSetMmThreadCharacteristicsWRun the time-sensitive loopAvRevertMmThreadCharacteristicsMMCSS allocates CPU preferentially

Figure 13: MMCSS is used as a register-and-revert pair. For continuous buffer processing it fits the Windows design better than running at high priority all the time.

4.4. Memory / GC / First-Run Costs

If you use new / malloc / List<T>.Add / string concatenation / LINQ on every pass through the hot path, the costs of collection and relocation will eventually surface. GC (garbage collection) itself is not the villain, but if you write allocation-heavy code, its impact will surface as jitter.

StartupAllocate the needed buffersTouch them once to warm the pagesGet JIT / DLL loading / first I/O out of the wayThen do the real measurement / real operation

Figure 14: Get buffer allocation, page warming, and first-run costs out of the way at startup, and only then move to real measurement and real operation.

Checklist

  • No per-iteration memory allocation / deallocation on the hot path
  • Required buffers are preallocated at startup
  • Pages are warmed by touching them once at startup
  • First JIT, first DLL load, and first I/O are not mixed into the real measurement
  • No huge structures or variable-length logs growing inside the loop
  • If VirtualLock is used at all, it is limited to a very small critical region

.NET-side checks

  • Time measurement uses Stopwatch / Stopwatch.GetTimestamp()
  • No LINQ, string concatenation, ToString(), or large log generation on the hot path
  • async/await is not brought into the hot path
  • Pre-warm-up and post-warm-up are evaluated separately

4.5. Power Settings / EcoQoS / Timer Resolution

This part is unglamorous but effective. However tight your code is, results will not stabilize if higher-level power control is bearing down hard.

Power management on ordinary WindowsRun on AC powerPower mode: leaning toward Best performanceA dedicated production power plan if neededKeep time-sensitive processes away from EcoQoSCheck how timer-resolution requests are handled

Figure 15: What to check around power: the supply, the power mode, EcoQoS, and how timer-resolution requests are handled.

Checklist

  • Production evaluation is done on AC power first
  • Settings > System > Power & battery > Power mode is set toward Best performance
  • Battery saver / power-saving-first modes are not active during runs
  • Vendor-specific utilities’ quiet / eco / battery-first modes have been checked
  • Time-sensitive processes are not carelessly placed under EcoQoS (power-efficiency-leaning QoS)
  • IGNORE_TIMER_RESOLUTION is not enabled on the time-sensitive process
  • Verified whether timer-resolution requests lose effect when minimized / hidden
  • Power settings for everyday use and for production / measurement / demos are kept separate

timeBeginPeriod is useful when used in an organized way, but it is not a cure-all.

  • Call it just before it is needed
  • Revert with timeEndPeriod when done
  • From Windows 10 version 2004 onward, it no longer has the fully global behavior of the past
  • On Windows 11, a process with windows that is fully hidden / minimized / not visible / not audible may not be guaranteed high resolution
  • Raising the resolution does not increase QPC’s precision

If power or QoS effects are suspected, check the power throttling state with SetProcessInformation.

PROCESS_POWER_THROTTLING_STATE state{};
state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
state.ControlMask =
    PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
    PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION;
state.StateMask = 0; // HighQoS (performance-leaning) + honor timer-resolution requests

if (!SetProcessInformation(
        GetCurrentProcess(),
        ProcessPowerThrottling,
        &state,
        sizeof(state)))
{
    throw std::runtime_error("SetProcessInformation failed");
}

ControlMask says which mechanisms you take control of yourself, and StateMask says whether each of those mechanisms is on or off. The example above selects two mechanisms as controlled and turns both of them off. That is a declaration of two things: do not drop this process into EcoQoS (keep it leaning toward HighQoS) and do not ignore its timer-resolution request. Setting ControlMask to 0 instead hands both back to the OS and its default behavior.

What the two masks in a power throttling setting doShows that ControlMask selects which mechanisms you control yourself and StateMask decides whether each of them is on or off, which lets you declare that the process must not drop into EcoQoS and that its timer-resolution request must not be ignored.ControlMask selects what you controlStateMask decides on or offDeclare: do not drop into EcoQoSDeclare: do not ignore the timer-resolution requestA ControlMask of 0 hands it back to the OS

Figure 16: What the two masks of SetProcessInformation do: select what you control, then declare on or off.

Calling it from C#

To do the same thing from C#, the P/Invoke declarations look like this.

// C# / .NET 8
using System.Runtime.InteropServices;

internal static class PowerQos
{
    [StructLayout(LayoutKind.Sequential)]
    private struct PROCESS_POWER_THROTTLING_STATE
    {
        public uint Version;
        public uint ControlMask;
        public uint StateMask;
    }

    private const uint PROCESS_POWER_THROTTLING_CURRENT_VERSION = 1;
    private const uint PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 0x1;
    private const uint PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION = 0x4;

    // The fifth member of PROCESS_INFORMATION_CLASS (4 when counting from 0) is ProcessPowerThrottling
    private const int ProcessPowerThrottling = 4;

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetProcessInformation(
        IntPtr hProcess,
        int processInformationClass,
        ref PROCESS_POWER_THROTTLING_STATE processInformation,
        uint processInformationSize);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetCurrentProcess();

    /// <summary>Opts out of both power-efficiency-leaning treatment and having the timer-resolution request ignored.</summary>
    public static void OptOutOfPowerThrottling()
    {
        var state = new PROCESS_POWER_THROTTLING_STATE
        {
            Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION,
            ControlMask =
                PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
                PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION,
            StateMask = 0,
        };

        if (!SetProcessInformation(
                GetCurrentProcess(),
                ProcessPowerThrottling,
                ref state,
                (uint)Marshal.SizeOf<PROCESS_POWER_THROTTLING_STATE>()))
        {
            throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
        }
    }
}

Call it once, as PowerQos.OptOutOfPowerThrottling();, before starting the time-sensitive loop. The process handle needs PROCESS_SET_INFORMATION access rights, but the pseudo-handle for the current process returned by GetCurrentProcess() has no problem there.

4.6. CPU Placement / Core Migration / Heat

For CPU placement, rather than jumping straight to pinning to specific cores (hard affinity / CPU pinning), it usually works better to start with something closer to soft affinity: please run mostly on these cores.

YesNoMeasure firstSetThreadIdealProcessor / CPU SetsImproved enough?Stop thereConsider SetThreadAffinityMask lastAlso check temperature / clocks / long runs

Figure 17: Start CPU placement from measurement, stop there if a soft hint is enough, and keep pinning to specific cores as the last resort.

Checklist

  • CPU placement is only touched after measuring
  • Not pinning to specific cores right away
  • SetThreadIdealProcessor or CPU Sets tried first
  • SetThreadAffinityMask treated as a last resort
  • Temperature, clocks, and thermal throttling checked over long runs
  • Laptop quiet / low-noise modes checked

As an order of operations, this flow is safe.

  1. Measure first
  2. If needed, ideal processor / CPU Sets
  3. If improvement is still needed, pin to specific cores

Pinning to specific cores looks like it should help, but it removes the OS’s escape routes, so used casually it can actually leave you with less room to maneuver.

4.7. Isolating Drivers / DPCs / ISRs / External Disturbances

When only max occasionally explodes, or the average is good but p99.9 is bad, it pays to suspect external disturbances beyond your own code.

YesNoYesNoYesNoYesNoLate / miss / max spike occurredIs your own processing time also long?Shorten the hot path / reduce allocation / remove I/OAre there DPC / ISR spikes?Check USB / Wi-Fi / Bluetooth / GPU / audio / storage / ACPI / driver updatesPage faults / GC / first-run costs?Preallocate / warm up / reduce heap pressureBattery / power saving / thermal effects?AC power / power settings / cooling / long-duration testsDig deeper with ETW / WPA / LatencyMon

Figure 18: The order to work through when a spike appears: your own processing, then DPC/ISR, then memory, then power and heat.

Checklist

  • Drivers around Wi-Fi / Bluetooth / USB / storage / GPU / audio have been checked
  • Have compared runs with unnecessary cloud sync, indexing, and auto-updates stopped
  • Also tested whether things degrade when minimized or with the display off
  • DPC / ISR trends inspected with LatencyMon or ETW
  • Whether your own processing is heavy and whether you are being stalled from outside are examined separately

5. Measurement and Evaluation

5.1. What to Record

At minimum, you want to capture these.

  • Scheduled period time
  • Actual start time
  • Actual finish time
  • Lateness (how late the start was relative to schedule)
  • Execution time
  • Missed deadline count
  • Consecutive missed deadline count
  • Queue depth
  • Drop count
  • CPU utilization
  • Per-core skew
  • DPC / ISR spikes
  • Page faults
  • Temperature / clock variation

Looking only at averages makes the essence hard to grasp. What hurts in production is the occasional large latency spike.

5.2. How to Read p99 / p99.9 / max

Metrics like p99 exist to look at the slow tail. Averages alone hide the occasional large delay.

Metric Meaning Intuition over 10,000 measurements
Average The smoothed overall value Spikes get buried easily
p50 The middle value Close to everyday feel
p95 The threshold where the slowest 5% starts to show The boundary excluding the slowest 500
p99 The threshold where the slowest 1% starts to show The boundary excluding the slowest 100
p99.9 The threshold where the slowest 0.1% starts to show The boundary excluding the slowest 10
max The worst case The single slowest run

Suppose the numbers line up like this (an example for explaining how to read the metrics, not measurements from any specific hardware).

  • Average: 0.8 ms
  • p99: 1.2 ms
  • p99.9: 3.5 ms
  • max: 28 ms

The story is: usually fast, but with occasional large spikes. On ordinary Windows, the real problems almost always live in this tail from p99 to max.

The difference between the average and the tail metricsShows that looking only at the average hides the occasional large delay while looking at p99 and p99.9 and max reveals the slow tail, and that on ordinary Windows the real problems appear there.Look only at the averageThe occasional large delay is hiddenLook at p99, p99.9, and maxThe slow tail becomes visibleThe real problems appear between p99 and max

Figure 19: A good average with a spiking max means usually fast but occasionally spiking. Catch it with the tail metrics.

Note that this article does not put numbers on the effect of the recommended setup. Latency and jitter shift easily with the CPU, the drivers, resident software, power settings, and how load is applied, so numbers from someone else’s machine are not evidence about yours. Instead, here is the procedure for producing the same shape of table on your own machine.

The minimum procedure for getting p99 on your own machine

  1. On the hot path, only record. Take lateness and execution time with Stopwatch.GetTimestamp() (or QueryPerformanceCounter in C++) and write them into a preallocated array. Do not compute averages or sort here
  2. Aggregate after you stop measuring. Sort, then pull out the value at each percentile position
  3. Repeat the same procedure under different conditions: before and after warm-up, AC versus battery, UI in the foreground versus minimized, with and without load from other processes (5.4)
  4. Every time you introduce one change, take the numbers again under the same conditions and compare
// C# / .NET 8. Aggregate after measurement has stopped
using System.Diagnostics;

// On the hot path, only write into the array (zero allocation)
long[] latenessTicks = new long[100_000];
int count = 0;

// Example: inside the periodic loop
// latenessTicks[count++] = Stopwatch.GetTimestamp() - scheduledTimestamp;

static double PercentileMs(long[] ticks, int count, double percentile)
{
    long[] sorted = ticks.AsSpan(0, count).ToArray();
    Array.Sort(sorted);

    int index = (int)Math.Ceiling(percentile / 100.0 * count) - 1;
    index = Math.Clamp(index, 0, count - 1);

    return sorted[index] * 1000.0 / Stopwatch.Frequency;
}

// Usage
// Console.WriteLine($"p50={PercentileMs(latenessTicks, count, 50):F3}ms");
// Console.WriteLine($"p99={PercentileMs(latenessTicks, count, 99):F3}ms");
// Console.WriteLine($"p99.9={PercentileMs(latenessTicks, count, 99.9):F3}ms");
// Console.WriteLine($"max={PercentileMs(latenessTicks, count, 100):F3}ms");

Stopwatch.Frequency is the number of counts per second, so dividing by it and multiplying by 1000 gives milliseconds. With too few samples, p99.9 is meaningless. If you want to talk about p99.9, collect at least 10,000 samples, and preferably 100,000.

5.3. What to Measure With

The toolkit is fairly standard.

  • In-app measurement First capture period / lateness / execution time / queue depth / drop yourself
  • ETW / WPR / WPA Dig into CPU, context switches, DPC / ISR, page faults
  • LatencyMon Get a bearing on driver-induced wobble
  • Temperature / clock monitoring Watch for thermal effects
In-app measurementp50 / p95 / p99 / p99.9 / maxmisses / drops / queue depthETW / WPR / WPAcontext switches / DPC / ISR / page faultsTemperature / clock monitoringPrioritize the improvements

Figure 20: Line up the results from in-app measurement, ETW, and thermal monitoring to decide which improvements come first.

Going all the way to WPA takes some effort, but it is highly effective for separating whether DPCs / ISRs are the cause, or your own processing is simply heavy.

How to get them, and the minimum usage

Tool How to get it Minimum procedure
WPR / WPA (Windows Performance Toolkit) Select Windows Performance Toolkit when installing the Windows ADK (Windows Assessment and Deployment Kit). The default location is C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit Open a command prompt as administrator and (1) start recording with wpr -start CPU, (2) run the problem workload for a few tens of seconds, (3) save with wpr -stop trace.etl "periodic latency investigation". Then open trace.etl in WPA. wpr -profiles lists the profile names you can use
LatencyMon Download it from the Resplendence Software site. The Home Edition for personal use is the free version Start it, begin measuring, and leave it running for a few minutes while your workload runs. It aggregates the maximum kernel timer latency plus the ISR / DPC execution times and hard pagefaults per driver, so note down any driver whose execution time stands out

The first things to look at in WPA are, among the CPU-related graphs, DPC / ISR execution time and context switches. They line up which driver held the CPU during the stretches when your thread could not run, so cross-reference them against the times of the delays you recorded in 5.1.

Note that this article includes no screenshots. Their appearance changes easily between versions, so follow the procedure above and check on the actual screen instead.

5.4. Testing Discipline

A quiet bench environment alone is not enough for testing. At minimum, you want to examine these conditions separately.

  • Right after startup, before warm-up
  • After warm-up
  • Long continuous runs
  • UI in the foreground
  • UI minimized / close to hidden
  • AC power
  • Battery power
  • With load on the network or disk

Evaluating only on the bench makes it easy to miss problems that appear in real operation. Ordinary Windows behavior is easily pulled around by how the machine is used, so it is important to verify under conditions close to actual use.

How to think about testing under separated conditionsShows that evaluating only in a quiet bench environment misses problems that appear in real operation, so conditions such as before and after warm-up, long runs, the UI in the foreground versus minimized, AC versus battery power, and the presence or absence of load should be checked separately.Evaluating only on a quiet benchMisses the problems of real operationEvaluate conditions separatelyWarm-up and long runsUI in the foreground and minimizedPower supply and load conditions

Figure 21: Ordinary Windows behavior is pulled around by how the machine is used. Evaluate under conditions close to actual use.

6. A Rough Guide to Choosing

The quick reference by period range has been moved up to 1.1 so that you can jump back to it while reading. This section adds the two judgment calls that the table alone does not settle.

Where to decide that ordinary Windows will not do

Once you have to hold under 1 ms over long runs under heavy load, deciding to move only the time-critical part outside is often faster than continuing to tune. The candidates to move it to are device-side firmware, a dedicated controller, an FPGA, and an RTOS. Base the decision on the numbers from chapter 5, not on impressions.

  • The hot path cannot be shortened any further, yet p99.9 and max keep exceeding the requirement
  • External disturbances (DPCs / ISRs, drivers, other processes) are the cause, and app-side measures cannot reach them (confirmed with the isolation steps in 4.7)
  • The requirement has shifted from surviving an occasional miss to never missing once

When you want everything to live in one process

Holding the GUI, logging, communication, and the database in the same loop of the same process means downstream concerns break upstream deadlines. Waiting on a file flush, reconnecting to a database, and repainting the UI can each stretch to tens of milliseconds. Widening the fast path / slow path separation (4.2) from threads to whole processes is also an option. Split the processes and the upstream period keeps running even when the downstream side seizes up.

One combined process versus separated processesShows that holding the GUI and logging and communication and the database in the same loop of the same process lets downstream concerns break upstream deadlines, and that widening the fast path and slow path separation to whole processes keeps the upstream period running even when the downstream side seizes up.Everything in one process and one loopDownstream concerns break upstream deadlinesFlush waits, reconnects, repaintsSplit upstream and downstream into processesUpstream keeps running when downstream seizes up

Figure 22: If everything really must live together, widening the fast path / slow path separation to whole processes is an option.

7. Conclusion

There are two premises worth holding on to.

  • What to aim for on ordinary Windows is not a hard real-time guarantee, but a soft real-time configuration: small latency and jitter, and no breakage when a deadline violation occurs
  • The biggest win is tidying the hot path, more than tuning priorities

On the implementation side, these are what pay off.

  • Separate the fast path and the slow path
  • Use fixed-length queues, and decide the overflow policy in advance
  • Measure with QPC, and wait with events or waitable timers
  • Avoid allocation, blocking I/O, and heavy locks on the hot path

On the operations side, these are what pay off.

  • Run on AC power
  • Keep a separate power configuration for production
  • Reduce unnecessary background load
  • Evaluate with p99 / p99.9 / max and miss counts

Soft real-time on ordinary Windows is not decided by priority settings alone. Work through design, implementation, power settings, measurement, and operations as separate concerns, and you can build a remarkably stable system.

8. 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.

Frequently Asked Questions

Common questions about the topic of this article.

Can Windows do real-time processing?
You cannot get a hard real-time guarantee of zero deadline violations, but if you work carefully through design, implementation, measurement, and operations, you can reach a genuinely practical state as soft real-time. Periodic processing from a few milliseconds to tens of milliseconds, buffer-driven audio and video, and sensor acquisition with a control loop are all realistic on ordinary Windows 10/11. Conversely, if you need a guarantee of zero deadline violations, or stability below a few hundred microseconds over long runs, you should consider moving the work to an RTOS, a dedicated controller, an FPGA, or the device side.
Why should I not use Sleep(1) for a periodic loop?
Sleep(1) does not give you a 1 ms period. It waits roughly 1 ms or more and then adds your processing time on top, so the wait overshoot accumulates directly. A periodic loop is stable when it runs on the absolute deadlines produced by next += period, when it waits on a device event or a high-resolution waitable timer, and when only the final fine adjustment is left to a very short busy-spin. Use timeBeginPeriod only while you need it, and revert it when you are done.
What has the biggest effect on reducing latency and jitter?
Making the hot path short, fixed-length, and non-blocking, rather than raising priorities. Split the fast path of acquisition and control from the slow path of storage, communication, and UI, connect them with a fixed-length queue, and keep file writes, network sends, heavy logging, and per-iteration allocation off the hot path. On the operations side, AC power, reviewing the power mode, checking EcoQoS, and pruning background load are what pay off.
Which metrics should I use to evaluate the stability of a periodic loop?
Look at p99, p99.9, max, and the missed deadline count, not just the average. An average of 0.8 ms with a max of 28 ms, for example, means the loop is usually fast but occasionally spikes badly, and on ordinary Windows the real problems show up in that tail from p99 to max. Record DPC/ISR spikes, page faults, queue depth, and temperature and clock variation alongside them, and evaluate separate conditions such as before and after warm-up, long continuous runs, a minimized window, and battery power.

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