What "Not Responding" Really Is — How Windows Decides an App Has Hung, and How to Design Apps That Don't
· Go Komura · Windows, Windows Development, Troubleshooting, Multithreading, WinForms, WPF, Win32 API, UI Design
“The app goes white mid-operation and shows (Not Responding).” “We get tickets that it hangs occasionally, but it never reproduces on a development machine.” — For Windows business apps, this “Not Responding” is one of the most common complaints. And what is surprisingly little known is that the thing putting up the “Not Responding” display is not the hung app itself — it is the OS.
How does Windows know that an app has “hung”? What is that frosted-white window? Aimed at developers writing business apps on Windows and at IT staff who take tickets about apps that hang, this article works through the “Not Responding” judgment from the basics of the message loop, and organises the classic causes of hangs, designs that do not hang, and a procedure for investigating the hung moment — all grounded in primary sources.
1. The Bottom Line First
- “Not Responding” is the OS’s judgment. When a window (and the GUI thread that owns it) is not waiting for input, is not in its startup sequence, and has not retrieved a message (
PeekMessage) for 5 seconds, the OS treats it as not responding. The judgment is not per-process.1 - The whitened window is a “ghost window”. The OS has hidden the original window and swapped in a fake of the same position, size, and appearance. All you can do is move, minimize, or close it; the contents are not running. A ghost window is not created while a debugger is attached.2
- The cause of a hang almost always comes down to one thing. The UI thread that should be pumping the message loop is blocked on heavy work or a wait. Synchronous I/O, network calls, lock waits, and
SendMessageacross threads are the classics.3 - The design principle is “do not wait or compute on the UI thread”. Move heavy work to a worker thread (
async/await+Task.Runin C#) and leave the UI thread devoted to painting, progress, and accepting cancellation.4 DoEventsand manually pumping the message loop are a breeding ground for reentrancy bugs. The “Not Responding” display goes away, but the structure now lets arbitrary events interrupt mid-work. Separation, not evasion, is the proper approach.- Investigation starts by capturing state at the hung moment. Take a dump and look at the UI thread’s stack, and you can almost always identify what it is waiting on.
2. Prerequisite: Windows Apps Are Driven by Messages
To understand “Not Responding”, you first need to take on board that a Windows GUI app is event-driven. A GUI app does not go and fetch input on its own; it receives messages the OS delivers (mouse, keyboard, repaint requests, timers, and so on) and acts on them.3
Each thread that creates a window has a message queue and runs a message loop like this.
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg); // the window procedure is called
}
GetMessage retrieves a message from the queue, and DispatchMessage calls that window’s window procedure (the message-handling function). Button-click handling, repainting, and WinForms or WPF event handlers all, when you boil them down, run inside one iteration of this loop.5
flowchart TB
accTitle: Basic structure of the message loop
accDescr: The OS places mouse, keyboard, and other input into the thread's message queue; the UI thread's loop retrieves it with GetMessage, calls the window procedure with DispatchMessage, and returns to the top of the loop when processing finishes
os["OS (input, repaint requests, timers)"] --> q["Thread message queue"]
q --> gm["Retrieve with GetMessage"]
gm --> dm["DispatchMessage"]
dm --> wp["Handle in the window procedure"]
wp --> gm
Figure 1: The heart of a GUI app is the message loop; every event handler runs as one iteration of this loop.
This structure has one important consequence. If you do time-consuming work inside the window procedure (an event handler), the loop cannot retrieve the next message in the meantime. It can react neither to clicks nor to repaint requests — that is what a “hang” really is.
It is also worth taking on board that messages are delivered by two paths. PostMessage places the message on the queue and returns immediately, and the loop retrieves and processes messages in order. SendMessage, on the other hand, calls the window procedure directly and does not return to the caller until processing completes.36 That difference carries straight into the deadlock discussion in Chapter 4.
flowchart TB
accTitle: Two message delivery paths
accDescr: PostMessage places the message on the queue and returns immediately; the message loop retrieves and processes messages in order. SendMessage calls the window procedure directly and does not return to the caller until processing completes
pm["PostMessage"] --> q2["Place on the queue (returns immediately)"]
q2 --> loop["Loop retrieves and processes in order"]
sm["SendMessage"] --> direct["Call the procedure directly"]
direct --> w2["Does not return until processing completes"]
Figure 2: Even though both “send a message”, a queued Post and a Send that waits for completion have completely different natures.
3. How “Not Responding” Is Judged — The 5-Second Rule and the Ghost Window
So how does the OS know that “this app has hung”? The criterion is documented officially. The OS treats a window as not responding when it is not waiting for input, is not in its startup sequence, and has not called PeekMessage (message retrieval) for 5 seconds.1 In other words, the OS watches whether “the message loop is actually turning” the way you would take a pulse, and if there is no pulse for 5 seconds it judges the window not responding (the documentation states that this 5-second value may change in the future). The unit of judgment is the window and the GUI thread that owns it; in an app with several UI threads, one thread hanging does not mean windows on another thread are dead. The thread to look at in a dump is the owner of the hung window.
What happens to a top-level window that has been judged is documented as well. The OS hides the original window and replaces it with a “ghost window” that has the same Z-order, position, size, and appearance. All the user can do with it is move, resize, or (forcibly) close it. The app inside is not actually responding, so no other operations work.2
flowchart TB
accTitle: Hung-window judgment and the ghost-window swap
accDescr: When the UI thread is blocked on heavy work and message retrieval stops for 5 seconds, the OS judges the window not responding, hides the original, swaps in a ghost window of the same appearance, and offers the user only move, minimize, and close
busy["UI thread blocked on heavy work"] --> stop["Message retrieval stops"]
stop --> judge{"5 seconds elapsed?"}
judge -->|"no"| stop
judge -->|"yes"| ghost["Swap in a ghost window"]
ghost --> u1["Title shows (Not Responding)"]
ghost --> u2["Frosted white; only move and close"]
Figure 3: Both the “Not Responding” text and the white screen belong to the ghost window the OS swapped in, not to the hung app.
The “(Not Responding)” string that appears on the title bar, and the frosted-white look under the Aero theme, both belong to this ghost window. Two practical consequences follow.
- By the time “Not Responding” is displayed, the thread that owns that window has not been processing messages for at least 5 seconds. It is not that “the display came up too soon” — the UI thread is definitely blocked.
- A ghost window is not created while a debugger is attached.2 When it looks as if “it never goes Not Responding under the debugger, but it does in release”, the hang itself can be the same and only the display different.
There is also an API, DisableProcessWindowsGhosting, that disables this swap for the whole process.7 It is meant for special cases such as kiosk terminals where you do not want the OS to make a window look operable on its own. Calling it stops the “Not Responding” display from appearing, but the fact that the app is hung does not change. Understand that this is not something a general app uses as a “Not Responding” countermeasure.
4. Why Apps Hang — Classic Patterns That Block the UI Thread
The cause, boiled down, is a single point — “the UI thread does not come back to the message loop” — but the shapes you meet in practice fall into a few classics.
flowchart TB
accTitle: Classification of classic causes that block the UI thread
accDescr: The four classic families — synchronous I/O and network calls, lock waits, SendMessage across threads, and COM STA involvement — all come down to the same single point that the UI thread cannot return to the message loop
kind{"Which classic cause?"}
kind --> io{"I/O or a lock?"}
kind --> other{"SendMessage or COM?"}
io --> c1["Sync I/O and network"]
io --> c2["Lock waits"]
other --> c3["SendMessage"]
c3 -.-> c3n["across threads"]
other --> c4["COM STA involvement"]
c1 --> core["UI cannot return"]
c2 --> core
c3 --> core
c4 --> core
core --> ar["Not Responding check"]
Figure 4: The visible symptom is the same, but the culprit blocking the thread falls into four families, and the countermeasure differs for each.
Synchronous I/O and network calls. This is the most common. The pattern of doing synchronously, inside a button-click handler, a large file read or write, a database query, a Web API call, or access to a file on a network drive. On a development machine it finishes in a fraction of a second, so you never notice; production network latency or a file-server hiccup turns it into a wait of tens of seconds, and you get tickets that “it hangs occasionally”. Network drives have long timeouts when the connection is down, and they make the symptom dramatically worse.
Lock waits. The pattern in which the UI thread tries to take a lock on data shared with a worker thread, and ends up waiting for a worker that holds that lock for a long time. Lock discipline is covered in detail in the practical multithreading series.
SendMessage across threads. SendMessage does not return until the destination window’s procedure has finished processing.6 When you send it to a window on another thread, the sender is made to wait until that thread is in a state where it can process messages. If the destination thread is itself waiting for something, you have a message deadlock in which each side waits for the other.3 Sending to HWND_BROADCAST in particular will drag you in as soon as a single window is not responding. When you cannot afford to wait, consider SendMessageTimeout or PostMessage, which does not wait for a reply.8
sequenceDiagram
accTitle: Deadlock caused by SendMessage across threads
accDescr: If a worker thread sends SendMessage to a UI-thread window while the UI thread is blocked waiting for the worker's result, each side waits for the other to finish and you have a deadlock
participant U as UI thread
participant W as Worker thread
U->>U: Waiting for the worker to finish (blocked)
W->>U: SendMessage (does not return until processed)
Note over U: Cannot process messages (blocked)
Note over W: Cannot return from SendMessage
Note over U,W: Waiting on each other — deadlock
Figure 5: “The UI thread waits for the worker, and the worker waits for the UI thread via SendMessage” is a classic deadlock.
COM apartment involvement. Calls to an STA object are delivered as window messages, so when the UI thread (STA) is blocked, COM calls from other threads get blocked as collateral as well. That structure is explained in the COM STA/MTA article.
The pile-up of “it’s only a moment”. Even a 50 ms synchronous call, invoked 100 times in a loop, is 5 seconds. The Not Responding threshold is 5 seconds, but perceived “sluggishness” starts around 100 ms. A design rule of thumb is “the UI thread may be blocked only for milliseconds”.
5. Designs That Don’t Hang — Moving Heavy Work Off the UI Thread
The design principle is one thing: move time-consuming work off the UI thread. In C# (WinForms/WPF), async/await is the shortest proper approach.
private async void RunButton_Click(object sender, EventArgs e)
{
runButton.Enabled = false;
try
{
// CPU-heavy work, or APIs that are only synchronous, go to a worker via Task.Run
var result = await Task.Run(() => HeavyCalculation(input));
// For I/O, use APIs that are natively async (they do not consume a thread either)
var data = await httpClient.GetStringAsync(url);
// After await you are back on the UI thread, so you can touch controls directly
resultLabel.Text = result + data.Length.ToString();
}
catch (Exception ex)
{
// An exception leaking from an async void handler will take the app down. Catch it here
MessageBox.Show($"The operation failed: {ex.Message}");
}
finally
{
runButton.Enabled = true;
}
}
There are three points. First, while await is waiting, the UI thread is back in the message loop, so you do not go Not Responding. Second, the continuation after await returns to the UI thread, so you can touch controls normally afterwards (touching a control directly from a worker thread is forbidden; if you need to, use Control.Invoke / Dispatcher.InvokeAsync).4 Third, disable the button while the work is running, and otherwise kill reentrancy by design.
The picture is the same in native Win32: hand the work to a worker thread, notify the UI thread of completion as a custom message via PostMessage, and update the UI in the window procedure. PostMessage only places the message on the queue and returns immediately, so the worker side is not blocked either.6 Write the waiting for the worker thread itself with the discipline covered in the condition-variable article.
flowchart TB
accTitle: Division of roles in an app that does not hang
accDescr: The UI thread is responsible only for accepting input, showing progress, and accepting cancellation; a worker thread runs the heavy work and returns completion to the UI thread via PostMessage or an await continuation
ui["UI thread: input, progress, cancel"] -->|"Hand off the work"| w["Worker thread: heavy work"]
w -->|"PostMessage / await continuation"| ui
ui -.-> ng["No synchronous I/O or long computation on the UI thread"]
Figure 6: Keep the UI thread as the “reception desk”, always hand heavy work to a worker, and take only the completion notification.
What you want to avoid is the technique of inserting Application.DoEvents() or a PeekMessage loop between chunks of heavy work just to keep the display alive. You dodge Not Responding, but arbitrary event handlers re-enter in the middle of the work. A second click of the button, closing the form during processing, a timer firing — any of them can corrupt data that is still being processed, and the bugs are timing-dependent and hard to reproduce. Keep manual pumping of the message loop inside a limited structure such as a modal progress dialog, and as a rule solve it with separation.
sequenceDiagram
accTitle: Timeline of a reentrancy bug caused by DoEvents
accDescr: Calling DoEvents in the middle of heavy work lets a queued click's event handler interrupt and run, rewrite data that is still being processed, and then resume the original work, producing timing-dependent data corruption
participant U as UI thread
U->>U: Heavy work starts (data being processed)
U->>U: DoEvents (process queued messages)
Note over U: The button-reclick handler interrupts
U->>U: The interrupting work rewrites the data
U->>U: Original work resumes (data already inconsistent)
Figure 7: DoEvents erases “Not Responding” in exchange for inviting arbitrary events into the middle of the work.
For long-running work, include progress display and cancellation in the design as well. Send progress to the UI with IProgress<T> and communicate interruption with a CancellationToken, and the user can see that “it is working” and will not reach for a forced termination (which is often a cause of data corruption).
flowchart TB
accTitle: Progress and cancellation flow for long-running work
accDescr: The worker thread sends progress to the UI thread via IProgress; a cancel action on the UI reaches the worker through a CancellationToken; the worker stops at a convenient boundary and cleans up
w3["Worker: long-running work"] -->|"Progress via IProgress"| ui2["UI: progress and a Stop button"]
ui2 -->|"CancellationToken"| w3
w3 --> stop2["Stop at a boundary and clean up"]
Figure 8: Progress is “worker → UI”; cancellation is “UI → worker”. Include this thin two-way channel in the design from the start.
6. Investigating the Hung Moment
In an investigation of “it hangs occasionally”, the most valuable thing is the thread state at the exact moment it is hung. Reboot, and the evidence is gone.
Take a dump. On Task Manager’s Details tab, right-click the target process → “Create dump file”. That alone gives you a full dump with every thread’s stack. Just telling the IT staff who take the tickets “when it hangs, take this before you close it” changes the success rate of the investigation a great deal. For building a collection mechanism, see the crash-dump collection article.
Look at the UI thread’s stack. Open the dump in WinDbg and look at the stack of the thread that is pumping the message loop (usually thread 0). Synchronous I/O shows up as ReadFile or a network API, a lock wait as a WaitFor… family call, and SendMessage across threads as waiting inside SendMessage — as-is. How to read it is explained in the introductory WinDbg article.
Look at it live. With Process Explorer you can inspect the thread list and stacks on the spot. When it is steadily sluggish, take a WPR trace and analyse the UI thread’s waits over time (WPR/WPA in practice).
flowchart TB
accTitle: Basic procedure for investigating Not Responding
accDescr: Take a dump at the hung moment, look at the UI thread's stack, identify whether it is stopped on synchronous I/O, a lock wait, or SendMessage across threads, and connect that to the matching design fix
hang["The hung moment"] --> dump["Take a dump (before closing)"]
dump --> stack["Look at the UI thread's stack"]
stack --> io["Synchronous I/O or network wait"]
stack --> lock["Lock wait"]
stack --> sm["SendMessage across threads"]
io -.-> fix["Separate the site onto a worker"]
lock -.-> fix
sm -.-> fix
Figure 9: The star of the investigation is “a dump of the hung moment”; the UI thread’s stack is itself the classification of the cause.
You can also standardise how you take a first cut from the symptom. If it always hangs on a particular operation, first suspect synchronous I/O inside that handler. If it hangs rarely and with no correlation to an operation, suspect lock ordering or a SendMessage deadlock across threads, and match up both threads’ wait targets in the dump. If it hangs only in a particular environment, suspect timeouts from environmental factors such as a network drive, a proxy, or antivirus software.
7. Summary
- “Not Responding” is a mechanism in which the OS judges that an app has not retrieved a message for 5 seconds and swaps in a ghost window. The thing putting up the display is the OS, not the app.
- The cause of a hang is a single point: “the UI thread cannot return to the message loop”. Synchronous I/O, the network, lock waits, and SendMessage across threads are the classics.
- The countermeasure is to move heavy work off the UI thread. In C#,
async/await+Task.Run; in Win32, a worker thread +PostMessage. Prevent reentrancy during execution by design, such as disabling the button. - Evading it with
DoEventsis in exchange for reentrancy bugs.DisableProcessWindowsGhostingonly removes the display. Neither is a root-cause fix. - For investigation, a dump of the “hung moment” is the most important thing. The cause is almost always written on the UI thread’s stack as-is.
From the user’s point of view “Not Responding” is “it’s broken”, but once you know the mechanism you can translate it into the precise sentence “the UI thread did not come back for 5 seconds”. Working backwards from that one sentence, the candidate causes, the fix, and the investigation procedure all fall out naturally.
Related Articles
- Spurious Wakeups — Why Condition Variables Wake “Without Being Notified” and How to Wait Correctly on Windows
- Practical Multithreading Best Practices: .NET Edition — What to Decide Before You Add More Threads
- COM STA/MTA Fundamentals - Threading Models and How to Avoid Hangs
- Reading Crash Dumps with WinDbg + SOS — A Practical Guide to Analysis After Collection
- Process Explorer / Handle / VMMap in Practice — Chasing Hangs, Leaks, and “File in Use” from the State Right Now
- Windows Shutdown as Seen from Your App — Surviving Exit Notifications, Restarts, and Power Loss Correctly
Related Consulting Areas
KomuraSoft LLC handles root-cause investigations of business apps that “occasionally hang” or go “Not Responding” (dump analysis and trace analysis), refactoring of legacy UI code full of synchronous work into async/await and worker-thread separation, and reviews of UI designs that do not freeze. Even when you do not yet have a reproduction procedure, we can help starting from the design of how to collect evidence.
- Bug Investigation & Root-Cause Analysis
- Technical Consulting & Design Review
- Windows Application Development
- Contact Us
References
-
Microsoft Learn, IsHungAppWindow function (winuser.h). On the judgment criterion that an app is treated as not responding when it is “not waiting for input, is not in its startup sequence, and has not called PeekMessage for the internal timeout of 5 seconds”; on this 5-second criterion being subject to change; and on the function always returning TRUE for a ghost window. ↩ ↩2
-
Microsoft Learn, GetMessage function (winuser.h). On the system treating a top-level window as not responding when it stops responding to messages for several seconds and replacing it with a ghost window of the same Z-order, position, size, and appearance; on the user being able only to move, resize, or close it; and on a ghost window not being created while a debugger is attached. ↩ ↩2 ↩3
-
Microsoft Learn, About Messages and Message Queues. On Windows apps being event-driven and the window procedure processing messages; on the distinction between queued messages and messages sent directly; on the swap of a not-responding window for a ghost window; and on the section covering deadlock from threads sending messages to each other. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). On WinForms controls not being safe to touch from any thread other than the one that created them; on using Invoke/BeginInvoke for updates from another thread; and on safe asynchronous patterns using async/await or BackgroundWorker. ↩ ↩2
-
Microsoft Learn, Using Messages and Message Queues. On a typical message-loop implementation with GetMessage, TranslateMessage, and DispatchMessage, and on how to inspect a message queue. ↩
-
Microsoft Learn, SendMessage function (winuser.h). On SendMessage calling the specified window’s window procedure and not returning until processing completes; on a send to a window on another thread making the sender wait until that thread processes the message; and on the difference from PostMessage, which places the message on the queue without waiting for a reply. ↩ ↩2 ↩3
-
Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). On being able to disable, for the calling GUI process, the ghost-window feature that makes a not-responding window minimizable, movable, and closable; and on the disablement lasting for the lifetime of the process. ↩
-
Microsoft Learn, SendMessageTimeout function (winuser.h). On being able to send a message with a timeout; and on a flag (SMTO_ABORTIFHUNG) that returns without waiting when the window is not responding (has been judged hung). ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
DllMain and the Loader Lock — The Real Reason You're Told to "Do Nothing in DLL Initialization"
Why you must not call LoadLibrary or synchronize with other threads from DllMain. Drawing on primary sources, this article explains how t...
The Win32 Thread Pool API — Concurrency Without Creating Threads, via CreateThreadpoolWork
Are you spawning CreateThread calls all over your native code? This article explains the Win32 thread pool API redesigned in Vista — the ...
Apps That Break on Resume from Sleep — How Windows Power Events Work and How to Build Business Apps That Survive Them
You opened the laptop and the business app's connections were dead — the cause is a design that never accounted for sleep. This article c...
Spurious Wakeups — Why Condition Variables Wake "Without Being Notified" and How to Wait Correctly on Windows
A condition variable's wait can return even when no notification has arrived (a spurious wakeup). This article explains, from the Windows...
How the Clipboard and Drag & Drop Work — Handling OLE Data Transfer Correctly in Business Apps
Paste an Excel table and the formatting falls apart; close the source app and you can no longer paste — both come from the clipboard plac...
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.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Frequently Asked Questions
Common questions about the topic of this article.
- Under what conditions does "Not Responding" appear?
- The OS judges a window hung when a windowed app is not waiting for input, is not in its startup sequence, and has not retrieved a message (PeekMessage) for 5 seconds. The hung top-level window is hidden and replaced with a "ghost window" of the same position, size, and appearance. The "(Not Responding)" title-bar text and the frosted-white look belong to this ghost window, which only lets you move, minimize, or close. In other words, "Not Responding" is not something the app itself displays — it is a screen the OS puts up on the app's behalf.
- Is there a setting to keep "Not Responding" from appearing while work is in progress?
- Calling DisableProcessWindowsGhosting disables the swap to a ghost window for that process. That only makes the hang less visible to the user, though — the window still does not react to input, and from the user's point of view it is a complete freeze with no way to move or close it. The real fix is not suppressing the display, but moving heavy work to a worker thread so the UI thread is never blocked for even a tenth of a second, let alone five. Note also that the OS does not create a ghost window while a debugger is attached, so it can look as if "Not Responding" never happens during debugging.
- Is it acceptable to avoid "Not Responding" with DoEvents (manually pumping the message loop)?
- It is not recommended. Spinning DoEvents or a PeekMessage loop in the middle of heavy work will dodge the hung-window judgment, but any event handler can then re-enter — a second click of the button, closing the window, a timer, and so on. Another handler rewriting data that is still being processed, or touching a form that was supposed to be closed and throwing, produces reentrancy bugs that are timing-dependent and hard to reproduce — worse than "Not Responding" itself. The proper approach is to move the work itself to a worker thread with Task.Run or similar, and leave the UI thread responsible only for progress display and accepting cancellation.
- How do I update the UI (controls) from a worker thread?
- WinForms controls and WPF elements may be touched only from the thread that created them (normally the UI thread). Touching them directly from a worker thread causes exceptions or undefined behaviour. In C#, async/await is the easiest path: the continuation after await returns to the calling UI thread, so you can update controls normally after the await. To switch explicitly, use Control.Invoke/BeginInvoke in WinForms and Dispatcher.InvokeAsync in WPF. In native Win32, the established pattern is for the worker thread to PostMessage a custom completion message to the UI thread, and for the window procedure to update the UI.
- How do I investigate why an app is showing "Not Responding"?
- The important thing is to capture state at the hung "moment itself". First take a full dump from Task Manager's Details tab with "Create dump file", then in WinDbg look at the stack of the UI thread (the thread running the message loop). Whether it is stuck in synchronous I/O, a network wait, a lock wait, or waiting on another thread via SendMessage shows up on the stack as-is. To look at a live process, Process Explorer's thread list and stack view are useful; to follow it over time, capturing a WPR trace is effective. See also this site's introductory WinDbg article, Process Explorer in practice, and WPR/WPA in practice.