Windows Shutdown as Seen from Your App — Surviving Exit Notifications, Restarts, and Power Loss Correctly

· · Windows, Shutdown, Windows Development, Windows Services, Device PCs, Data Integrity, Long-Running Operation, UPS

“After a Windows Update overnight restart, the measurement app on the device PC went down mid-write, and by morning the measurement file was corrupted.” “Someone signed out of a shared PC and complained that unsaved edits had vanished.” — For long-running Windows apps, these two consultations are classics.

What both sites have in common is treating shutdown as “an abnormal event that should not happen”. In reality, though, from Windows Update automatic restarts, user sign-out, and a UPS-initiated shutdown through to an unannounced power loss, events that cut execution from outside the app will come, sooner or later. You cannot prevent them from coming. What you can prevent is “losing data when they come”.

Fortunately, Windows has a mechanism that notifies the app before shutdown, for GUI apps, console apps, and services alike. Aimed at IT staff in small and medium businesses and at Windows app developers (especially of device-PC and long-running apps), this article organizes how to receive those notifications, how to design cleanup that “finishes in a few seconds”, automatic recovery after a restart, and how to prepare for a power loss that brings no notification — all grounded in Microsoft Learn primary sources as of August 2026.

1. The Bottom Line First

  • Design shutdown as “a normal event that will come, sooner or later”. The time you can use after receiving the notification is, in principle, only about 5 seconds, so a design that frantically saves everything on the spot will break down. The prerequisite is frequent autosave so that “the delta that must be saved at shutdown” stays small.1
  • On client OSes from Windows 8 onward, when fast startup is enabled (the default on most PCs that support hibernation), “Shut down” is a hybrid shutdown, and the kernel is only hibernating. The only thing that is fully reset is “Restart”. That is the real reason “I shut it down and it didn’t get better, then I restarted and it did”.2
  • A GUI app should return TRUE immediately to WM_QUERYENDSESSION, and do cleanup in WM_ENDSESSION. In principle you must not return FALSE (refuse).1
  • Only when you truly have an operation that cannot be interrupted should you display a reason with ShutdownBlockReasonCreate. Even then the user and the OS can force continuation, so a design that assumes “we can block” does not hold.34
  • A console app receives the notification with SetConsoleCtrlHandler. The grace period is even shorter — a default of 5 seconds for console close. There is also a trap: in a process that has loaded gdi32.dll or user32.dll, some of these events do not arrive.56
  • Cleanup that relies on .NET’s AppDomain.ProcessExit does not run, from .NET 10 onward, on paths where the process is “terminated from outside”. On a normal exit such as returning from Main it still runs as before, but because the runtime no longer provides default handling for termination signals such as console close and shutdown, cleanup on those paths must move to the notification that matches the app model.7
  • A Windows service can receive SERVICE_ACCEPT_PRESHUTDOWN earlier, and with a configurable grace period, than SERVICE_ACCEPT_SHUTDOWN (about 20 seconds of grace). The default PRESHUTDOWN timeout, however, was shortened to 10 seconds from Windows 10 Creators Update onward, so either way you need a design that does not lean too hard on the grace period.89
  • Automatic recovery after a restart can be achieved by combining RegisterApplicationRestart with ARSO (automatic sign-on). Recovery paths are provided for crash, not responding, and update-driven restart.1011
  • A power loss brings no notification at all. The standard pattern is to write completely to a temporary file, flush, and swap with ReplaceFile, but because ReplaceFile does not guarantee atomicity across a power loss either, a recovery path of backup (.bak) plus load-time validation is part of the set. After-the-fact isolation can be done from the event log (1074/41/6008).121314

In one sentence, the conclusion of this article is: “always keep a state from which you can close the shop in a few seconds when the notification arrives, and write in a way that does not break even on a power loss that brings no notification”.

2. What Happens at Shutdown — Four Ways to End

2.1. Sign-out, shutdown, restart, and power loss

From the app’s point of view, what matters are two axes: “how the user session ends” and “what happens to the kernel”.

Operation User session Kernel and drivers Notification to the app
Sign-out Ends Keeps running WM_QUERYENDSESSION (ENDSESSION_LOGOFF) → WM_ENDSESSION
Shutdown (fast startup enabled) Ends Hibernates (saved to hiberfil.sys) WM_QUERYENDSESSION → WM_ENDSESSION, (PRE)SHUTDOWN to services
Restart Ends Ends completely; next boot is a full boot Same as above
Power loss Vanishes immediately Vanishes immediately None

Sign-out and shutdown are, from the app’s point of view, almost the same event. If the ENDSESSION_LOGOFF bit is set in WM_QUERYENDSESSION’s lParam it is a sign-out; if it is 0 it is a shutdown or a restart (you cannot tell the two apart).1 In other words, the complacency of “it’s only a sign-out, we’ll be fine” does not hold, and the correct design is for the same cleanup code to be called.

Four ways to end, and the notification to the appSign-out, shutdown, and restart deliver the WM_QUERYENDSESSION to WM_ENDSESSION notification, and cleanup finishes in a few seconds. Only a power loss has no notification at all, so you prepare with the write design in Chapter 8 and a UPSSign-outQUERY → ENDSESSIONShutdownRestartPower lossNo notify: write + UPSCleanup in seconds

2.2. The real reason “I shut it down and it didn’t get better” — hybrid shutdown

The easy-to-miss row is the second one in the table. On client OSes from Windows 8 onward, fast startup (hybrid shutdown) is enabled by default on PCs that support hibernation, and the behavior of “Shut down” changed. Sign-out of the user session still happens as usual, but the kernel session is not closed; it is saved, device drivers and all, to the hibernation file (hiberfil.sys) and restored as-is on the next boot. That makes startup faster, but kernel and driver state survive even after you cut the power.2 This is, however, conditional behavior. In an environment where hibernation itself is disabled (powercfg /hibernate off), where policy or Power Options has turned fast startup off, and on Windows Server, shutdown is a conventional full shutdown. You can tell which way a given PC is running from the “Turn on fast startup” check box in Power Options, or from whether powercfg /a (available sleep states) lists “Fast Startup”.

What happens to the kernel on a Shut down operationA Shut down operation splits into a full shutdown or kernel hibernation depending on whether fast startup is on, and Restart always does a full bootFast startup onHibernate off / ServerShut downRestartSession ends + kernel hibernateFull shutdownNext: restore kernelNext: full boot

“Restart”, on the other hand, always runs a complete boot cycle. After a driver update, for example, you need a completely new state.2 From that, several phenomena you hear in the field fall into place.

  • “I shut it down and powered it back on, but the device trouble didn’t go away” — the kernel and drivers were only restored from hibernation; they were not reset
  • “It got better after I restarted” — because a full boot initialized them
  • Device-PC incident procedures should say “Restart”, not “power it off and on”

If you want to make a full shutdown explicit from the command line, shutdown /s (Shutdown.exe’s default is a full shutdown); if you want the default hybrid behavior, shutdown /s /hybrid.2 Disabling fast startup is not recommended. The app side should assume “on shutdown the kernel may only be hibernating” — for example, do not estimate “cumulative uptime” from the OS boot time — and design so that it does not break either way (whether fast startup is on or off differs by environment).

3. How a GUI App Should Behave — WM_QUERYENDSESSION and WM_ENDSESSION

3.1. How the two messages split the work

An app that has a window and a message queue is notified of session end in two stages.1

  1. WM_QUERYENDSESSION — a query: “is it all right to end?” The app should return TRUE immediately; DefWindowProc’s default response is also TRUE. Do not start cleanup here.
  2. WM_ENDSESSION (wParam=TRUE) — a committed notification: “the session really is ending”. Cleanup happens here.

Returning FALSE to WM_QUERYENDSESSION can abort shutdown, but the documentation is explicit that “you should return TRUE and respect the user’s intent”, and an app that returned FALSE is still exposed in the full-screen UI as “an app that is preventing shutdown”. Console apps and apps with no visible window cannot abort shutdown in the first place, and if they do not respond within 5 seconds they are terminated automatically.14

Flow of the two-stage session-end notificationReturning TRUE to the WM_QUERYENDSESSION query commits with WM_ENDSESSION and cleanup runs. Refusing with FALSE displays the app as one that is preventing shutdown, and about 5 seconds of no response can force continuationTRUE (rule)FALSE (refuse)No reply ~5sForce continueCancelWM_QUERYENDSESSIONWM_ENDSESSION (committed)Shown as blocking shutdownTreated as hungCleanup hereProcess exitShutdown aborted

3.2. What happens if you do not respond — the 5-second wall

On both WM_QUERYENDSESSION and WM_ENDSESSION, you can delay the response by about 5 seconds. Beyond that, the system displays the “This app is preventing shutdown” screen, and the user can choose to force continuation (= force-terminate the app).4 A force-terminated process is not given another chance to finish its save.

The design points are therefore these two.

  • Keep cleanup to an amount that finishes within 5 seconds. Microsoft itself recommends saving data frequently in ordinary operation so that less must be saved at shutdown, and saving unsaved data to a temporary location to restore on the next launch.1
  • Do not put up a confirmation dialog during shutdown. While you sit waiting on “Do you want to save?”, the 5 seconds pass. Silently fall to the safe side (autosave).

3.3. Implementation in WinForms and WPF

In a .NET desktop app, these messages are translated into framework events. In WinForms, FormClosing is raised, and CloseReason tells you whether shutdown is the cause.

// WinForms: FormClosing is also raised on shutdown / sign-out
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    if (e.CloseReason == CloseReason.WindowsShutDown)
    {
        // Do only an idempotent snapshot save. Do not show a dialog.
        // Do not set e.Cancel = true (refuse) either.
        SaveWorkingStateToTempFile();
        return;
    }

    // For ordinary closes such as the user clicking the × button, you may confirm here
}

In WPF, the Application.SessionEnding event (the XAML SessionEnding attribute, or an OnSessionEnding override) corresponds.

How WinForms/WPF events map to the messagesThe query phase of WM_QUERYENDSESSION maps to WinForms FormClosing and WPF SessionEnding, and what you do there is an idempotent snapshot save at most. There is no corresponding event for the committed WM_ENDSESSION, so receive it in WndProc or a hook and do cleanup that can run only after commitWM_QUERYENDSESSIONWinForms: FormClosingWPF: SessionEndingIdempotent snapshot onlyWM_ENDSESSIONNo event: WndProc hookCleanup after commit
// WPF: App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
    base.OnSessionEnding(e);

    // You can distinguish ReasonSessionEnding.Logoff / Shutdown,
    // but the baseline is to run the same snapshot save in either case
    SaveWorkingStateToTempFile();

    // Do not set e.Cancel = true unless you have an exceptional reason
}

There is one caveat here. Both FormClosing (CloseReason.WindowsShutDown) and WPF’s SessionEnding correspond to the query phase (WM_QUERYENDSESSION). If another app refuses, shutdown is aborted and your app keeps running. Therefore what you may do in these events is an idempotent snapshot save that does no harm if shutdown is aborted and produces the same result no matter how many times it runs. If you need “cleanup that must be done only when we are actually ending” (disconnecting, handing resources back, and so on), hook the committed WM_ENDSESSION (wParam=TRUE) directly in WndProc and do it there.

On either path, fold the body into a common “snapshot save” function and write restore data for a normal exit, a shutdown, and (if possible) a crash in the same format, so that restore logic on the next launch is a single path. Designing to leave information even on a crash is covered in “Designing Windows Apps to Leave Logs and Dumps When They Crash”.

4. If You Really Must Block — ShutdownBlockReasonCreate

Operations that physically break if they are cut mid-way, such as writing a CD or firmware, are the exception. The correct practice here is to register a reason string with ShutdownBlockReasonCreate when the uninterruptible operation starts, and call ShutdownBlockReasonDestroy immediately when it finishes. When shutdown is requested, that reason is displayed on the “This app is preventing shutdown” screen, and the user can decide whether to continue or cancel.3

Flow of protection with ShutdownBlockReasonCreateRegister a reason when the uninterruptible operation starts; if a shutdown request arrives while protected, the reason is shown full-screen and WM_QUERYENDSESSION is refused with FALSE. The user can cancel or force continuation, and the reason is cleared when the operation finishesCancelForce continueStart uninterruptible workShutdownBlockReasonCreateRun on a worker threadFinish: DestroyShutdown during thisShow reason + FALSEProcess exit
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, string pwszReason);

[DllImport("user32.dll", SetLastError = true)]
static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd);

// Call from the thread that created the main window (it fails from other threads)
_criticalOperationInProgress = true;
ShutdownBlockReasonCreate(this.Handle, "Writing measurement data to a file");
try
{
    // Run the uninterruptible operation on a worker thread. If you run it
    // synchronously on the UI thread the message pump stops, and the process
    // is force-continued as "Not Responding" before the WM_QUERYENDSESSION
    // refusal code below can run
    await Task.Run(() => WriteMeasurementData());
}
finally
{
    ShutdownBlockReasonDestroy(this.Handle);
    _criticalOperationInProgress = false;
}

// Also, refuse WM_QUERYENDSESSION with FALSE only while protected
protected override void WndProc(ref Message m)
{
    const int WM_QUERYENDSESSION = 0x0011;
    if (m.Msg == WM_QUERYENDSESSION && _criticalOperationInProgress)
    {
        m.Result = IntPtr.Zero;   // Refuse. The registered reason string is shown in the full-screen UI
        return;
    }
    base.WndProc(ref m);
}

The easy misunderstanding here is the split of roles. All ShutdownBlockReasonCreate does is register a reason string; it does not itself stop shutdown. What actually holds shutdown back is your own handling that returns FALSE to WM_QUERYENDSESSION while a protection flag is set, as above. Use the two as a set, and clear both immediately when the operation finishes. Also, run the protected operation itself on a worker thread and keep the UI thread able to process messages — the refusal mechanism only works once the message arrives (and even then the user and the OS can force continuation, so a write design that does not break “if it did not stop” — Chapter 8 — is still required).

There are three operational caveats.

  • Keep the reason string short and specific. The user is in a hurry and will only read for a few seconds. The documentation itself gives “Burning a CD” as an appropriate example.3
  • Do not leave it registered for the whole life of the app. “Only while an uninterruptible operation is in progress” is what the API assumes.
  • Do not design on the assumption that you can block. The user can choose to force continuation, and a forced shutdown (ENDSESSION_CRITICAL) will not wait in the first place. The documentation is explicit: “Applications should not depend on being able to block shutdown”.4

5. How Console Apps and Background Processes Should Behave

5.1. SetConsoleCtrlHandler and a short grace period

A console app cannot receive window messages, so control signals arrive at a handler function registered with SetConsoleCtrlHandler. The default grace period per signal is as follows.5

Signal When it occurs Default grace period
CTRL_C_EVENT / CTRL_BREAK_EVENT Ctrl+C / Ctrl+Break No timeout
CTRL_CLOSE_EVENT Closing the console, Task Manager “End task” (a forced process kill from the “Details” tab is an immediate exit with no notification, and is outside this table) About 5 seconds
CTRL_SHUTDOWN_EVENT System shutdown (service processes) About 20 seconds

There are two points to watch. First, essentially only a process running as a service can receive CTRL_LOGOFF_EVENT and CTRL_SHUTDOWN_EVENT. An app in an interactive session is terminated at sign-out, so a design that waits for these signals does not hold.5 Second, a process that has loaded gdi32.dll or user32.dll is treated as a Windows app even if you think of it as a console app, and the LOGOFF/SHUTDOWN handlers are not called. The official workaround is to create a hidden window and receive WM_QUERYENDSESSION/WM_ENDSESSION.6

Grace period per console signalCtrl+C and Ctrl+Break have no explicit timeout; console close has about 5 seconds and a shutdown signal to a service process has about 20 seconds; exceeding that force-terminates the processNo timeoutAbout 5sAbout 20sCTRL_C / BREAKHandlerRoutine cleanupCTRL_CLOSECTRL_SHUTDOWNForce-kill after grace
// Console app: clean up on Ctrl+C and console close
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);

delegate bool HandlerRoutine(int ctrlType);   // 2 = CTRL_CLOSE_EVENT

static readonly HandlerRoutine s_handler = OnCtrlEvent;  // Keep a reference so GC does not collect it

static bool OnCtrlEvent(int ctrlType)
{
    // Do only cleanup that finishes within 5 seconds
    FlushAndCloseDataFile();
    return false;   // Proceed to the default handler; the process exits
}

static void Main()
{
    SetConsoleCtrlHandler(s_handler, add: true);
    // ...
}

5.2. The .NET pitfall — do not rely on ProcessExit

In .NET there has long been a stock pattern of “just clean up in AppDomain.ProcessExit”, but from .NET 10 the runtime no longer provides default termination-signal handlers, and neither ProcessExit nor AssemblyLoadContext.Unloading fires on CTRL_CLOSE_EVENT/CTRL_SHUTDOWN_EVENT. The OS default handler simply terminates the process immediately.7

How ProcessExit changed in .NET 10Through .NET 9 the runtime's default signal handler received the termination signal, raised ProcessExit, then exited. From .NET 10 the runtime provides no default handler, the OS default handling terminates the process immediately, and you register a handler yourselfCTRL_CLOSE / SHUTDOWNThrough .NET 9: ProcessExitFrom .NET 10: immediate exitRegister a handler yourself

Instead, move to the canonical path for each app model.

  • GUI app: FormClosing / SessionEnding from the previous chapter
  • Generic Host (including Worker Service): IHostApplicationLifetime and BackgroundService.StopAsync. Make the stop grace period explicit with HostOptions.ShutdownTimeout
  • Bare console app: SetConsoleCtrlHandler (or subscribe to SIGINT/SIGTERM equivalents with PosixSignalRegistration)
Where each app model receives the exit notificationA GUI app uses FormClosing and SessionEnding plus a WM_ENDSESSION hook for committed work; Generic Host uses IHostApplicationLifetime and StopAsync; a bare console app uses SetConsoleCtrlHandler or PosixSignalRegistration. Relying on ProcessExit does not fire on external-signal pathsGUINot GUIHostConsoleWhich app model?FormClosing / SessionEndingHost or console?ENDSESSION hookLifetime + StopAsyncSetConsoleCtrlHandlerSet ShutdownTimeoutDo not rely on ProcessExit

The grace period differs by path — about 5 seconds for GUI and console close, the SCM grace period of Chapter 6 for a service (about 20 seconds, or the configured value for PRESHUTDOWN), and no explicit timeout for Ctrl+C. On every path, though, the grace period is limited and cannot be counted on, so the design axis is that the normal case is “already saved at each processing checkpoint”, not “work hard in the exit event”.

6. How a Windows Service Should Behave — SHUTDOWN and PRESHUTDOWN

6.1. Two kinds of shutdown notification

A service is not affected by sign-out, but it is stopped on shutdown and restart. The notification arrives as a control code from the Service Control Manager (SCM), and receiving it requires declaring an acceptance flag.8

Declaration Notification that arrives Timing and grace period
SERVICE_ACCEPT_SHUTDOWN SERVICE_CONTROL_SHUTDOWN Notified during shutdown processing. Default about 20 seconds, upper bound WaitToKillServiceTimeout
SERVICE_ACCEPT_PRESHUTDOWN SERVICE_CONTROL_PRESHUTDOWN Notified before SHUTDOWN. The SCM waits until the service stops or the timeout
Order of shutdown notifications to a serviceWhen shutdown starts, services that declared PRESHUTDOWN are notified first with the configured grace period, then the SHUTDOWN notification is sent with a default of about 20 seconds, and the process is terminated when the grace period expiresShutdown startsPRESHUTDOWN (if declared)SHUTDOWN (about 20s)Grace expires → exit

The PRESHUTDOWN timeout can be configured with ChangeServiceConfig2 (SERVICE_CONFIG_PRESHUTDOWN_INFO); the default is 10 seconds from Windows 10 Creators Update (build 15063) onward, and 3 minutes before that.9 If you are still working from the old knowledge that “PRESHUTDOWN gives you 3 minutes”, on a current OS you have only 1/18 of the grace period you expected. Also, PRESHUTDOWN holds up shutdown of the whole system for that interval, so the documentation too says it “should be used only in special circumstances”.8

Handler-side practice matters as well. The control handler must return within 30 seconds; leave time-consuming stop work to another thread, report SERVICE_STOP_PENDING, and return immediately.8

// Win32 service: accept PRESHUTDOWN and leave stop work to a worker
g_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN;

DWORD WINAPI ServiceHandlerEx(DWORD control, DWORD, LPVOID, LPVOID)
{
    switch (control)
    {
    case SERVICE_CONTROL_PRESHUTDOWN:
    case SERVICE_CONTROL_STOP:
        ReportStatus(SERVICE_STOP_PENDING, /*waitHintMs*/ 3000);
        SetEvent(g_stopEvent);   // Tell the worker to stop and return immediately
        return NO_ERROR;
    }
    return ERROR_CALL_NOT_IMPLEMENTED;
}

// Worker side: if cleanup runs longer than waitHint, keep reporting
// SERVICE_STOP_PENDING periodically while incrementing dwCheckPoint.
// The SCM judges "still alive and making progress" from waitHint and
// checkpoint advance. If reporting stops it can be treated as hung and
// shutdown can proceed. Always report SERVICE_STOPPED when finished

6.2. A design that does not rely on the grace period

Extending the grace-period ceiling, WaitToKillServiceTimeout, by rewriting it from the service side is explicitly not recommended. The documentation asks for the opposite — a service should finish cleanup as quickly as possible so that a UPS-powered machine can complete shutdown before the battery dies. The guidance is to save frequently in ordinary operation so that unsaved data is minimized, not to spend time freeing memory at shutdown, and not to wait too long for a reply when notifying a network peer. Also, the SCM at shutdown does not, by default, consider dependencies, so stop processing must still work “even if a service you depend on has already gone down”.8

Designing stop processing that does not rely on the grace periodIf you save at each processing checkpoint so that unsaved data is always minimal, cleanup when the stop notification arrives finishes in a few seconds. A design that saves everything at exit will not fit the grace period, and a force-termination loses the dataSave at each checkpointStop → save sliver → doneSave everything at exitStop → save misses graceForce-kill → data loss

In a .NET Worker Service (UseWindowsService), SERVICE_CONTROL_STOP and SHUTDOWN are translated into host stop, and BackgroundService.StopAsync is called. The stock implementation as of this writing accepts the STOP/SHUTDOWN family; if you need PRESHUTDOWN as well you will need an extended handler. Either way, make HostOptions.ShutdownTimeout explicit and finish StopAsync in a few seconds. For building a service in general, see “How to Build and Operate Windows Services”.

7. Recovering Automatically After a Restart

On a device PC or an unattended PC, the design scope is not only “survive shutdown” but “come back on your own after a restart”.

7.1. RegisterApplicationRestart and the recovery callback

If you have called RegisterApplicationRestart, the app is registered as a restart candidate for crash (unhandled exception), not responding, an update-driven app restart, and an update-driven OS restart. You can register command-line arguments for the restart, so if you include “which file was open” and “which restore point”, you can resume from where you left off after the restart.10

The specifications to take on board are as follows.10

  • Registration must be finished before the problem occurs (during WM_QUERYENDSESSION handling is the last chance in an update scenario)
  • To prevent a restart loop, a process that has been running for less than 60 seconds is not restarted
  • A process running elevated is not a candidate for automatic restart (the process cannot be recreated without elevation consent). Automatic recovery of an app that needs elevation is designed by keeping the UI at standard privilege and isolating privileged work in a service, or by an explicit launch path such as a Task Scheduler task “Run with highest privileges”
  • Restart after a crash or hang goes through the user’s consent; restart after an update is automatic
  • To recover across an OS restart, the side that requests the restart (an installer and the like) must call the shutdown API with the EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS flags

If you also register RegisterApplicationRecoveryCallback, WER (Windows Error Reporting) calls the callback on a crash and gives you a grace period to save in-progress data. If the save takes time, though, you must keep calling ApplicationRecoveryInProgress within the ping interval specified at registration or the recovery work is cut off mid-way. When the save is finished, notify completion with ApplicationRecoveryFinished. “Replacing an in-use file and restarting” at app-update time is Restart Manager’s territory, covered in detail in “How to Replace an exe or DLL That Is In Use”.

7.2. ARSO — automatic sign-in after an update restart

After a Windows Update restart, if no one signs in, user-session apps do not come back. What fills that gap is ARSO (Winlogon Automatic Restart Sign-On). When Windows Update starts a restart, it securely saves the last interactive user’s credentials, configures Autologon, and after the restart automatically signs that user in and then locks the screen.11 There is also a command such as shutdown /g that requests a restart plus resume of registered apps. Some environments disable this with organizational policy (DisableAutomaticRestartSignOn and the like), so when you design unattended recovery, check this setting as a set. And if you are relying on automatic launch in a user session for background work you always need, the right move is to make it a Windows service from the start.

Path by which an app recovers automatically after a restartIf you register with RegisterApplicationRestart before a problem occurs, the app is restarted after user consent on a crash or not-responding, and after ARSO automatic sign-in and screen lock on an update-driven restart. Processes under 60 seconds of runtime and elevated processes are out of scopeConsentRegisterApplicationRestartCrash or hangUpdate restartApp restartARSO sign-in + lockNot: under 60s / elevated

8. Surviving a Power Loss That Brings No Notification — Write Design and UPS

8.1. A write that “does not break no matter when it is cut” — temporary file + ReplaceFile

A tripped breaker, a failed PSU, or a pulled plug brings neither WM_ENDSESSION nor PRESHUTDOWN. As long as you “overwrite the original file in place” for settings or measurement results, a mid-write power loss can leave a broken file that mixes old and new.

The standard pattern is to write completely to a temporary file on the same volume and then swap. ReplaceFile packages the sequence “save to the new file → set the original aside → rename → delete” into a single API, and also carries over the original file’s attributes such as creation time, ACL, and alternate streams (the three files must be on the same volume).12 .NET’s File.Replace calls this as-is.

Save and recovery flow with a temporary file and ReplaceFileOn save, write completely to a temporary file, flush, and swap with ReplaceFile, leaving the old contents in .bak. On the next launch, validate the primary file and fall back to .bak if it is brokenOn next launchOn saveIntactBrokenPower loss at any stepValidate primaryUse as-isFall back to .bakFlush to diskWrite a complete temp fileReplaceFile → .bak
// The standard pattern for settings and data: write completely to a temporary file, then swap, and keep the old contents
public static void SaveAtomically(string path, string content)
{
    string dir = Path.GetDirectoryName(path)!;
    string tmp = Path.Combine(dir, Path.GetRandomFileName());  // Create it on the same volume

    try
    {
        using (var fs = new FileStream(tmp, FileMode.CreateNew, FileAccess.Write))
        using (var writer = new StreamWriter(fs))
        {
            writer.Write(content);
            writer.Flush();
            fs.Flush(flushToDisk: true);   // FlushFileBuffers equivalent. Write the OS buffer
                                           // out to disk (device-side cache limits are in 8.2)
        }

        if (File.Exists(path))
            File.Replace(tmp, path, path + ".bak");  // Calls ReplaceFile. Keep the old contents as .bak
        else
            File.Move(tmp, path);
    }
    catch
    {
        // If we fail mid-way, do not leave the temporary file. Repeated failures
        // of a periodic save would fill the volume with complete copies
        try { File.Delete(tmp); } catch { /* Prefer the original exception if delete fails */ }
        throw;
    }
}

With this, ordinary operation always leaves you able to read either “a complete old file” or “a complete new file”. ReplaceFile is, however, a multi-step namespace operation, and atomicity across a power loss is not guaranteed by specification. That is why the example above keeps a backup (.bak) — the read side validates the primary file at launch and falls back to the backup if it is broken, as a set. You cannot use this for append-only logs or CSV, so those use a format that builds breakage in, such as “one line = one record, and discard a broken last line on read”.

8.2. Success from WriteFile is not arrival at the disk

The other premise is that even if WriteFile returns success, the data may still only be in the OS cache. Windows puts file reads and writes on the system buffer and reflects them to disk periodically with lazy writing. To get data to disk for certain, either flush explicitly with FlushFileBuffers, or specify FILE_FLAG_WRITE_THROUGH at CreateFile so that each write goes through the cache. File-system metadata is always cached, so confirming metadata also needs a flush or write-through.13

Calling FlushFileBuffers every time is inefficient, though, and the documentation too encourages considering FILE_FLAG_NO_BUFFERING+WRITE_THROUGH instead of frequent calls.13 In practice, “flush only at a transaction checkpoint or just before closing the file” is a realistic compromise. The mechanics of this layer — the cache manager, lazy writing, and the fact that “I flushed and it still may not have reached the disk” because of hardware cache — are covered in depth in “Cache Manager: When Does Your WriteFile Actually Reach the Disk?”.

8.3. UPS and battery monitoring — turning a power loss into a shutdown

The real countermeasure against power loss on a device PC is a UPS. Think of the UPS’s role not as “stopping a power outage” but as turning “a power loss with no notification” into “a planned shutdown with a notification”. The design is a two-stage setup.

  1. Designing the grace period: UPS battery hold time > the sum of “detect the switch to battery → app and service cleanup → OS shutdown complete”. If service stop processing is slow, this equation no longer holds (Section 6.2)
  2. Detection: The switch from AC to battery, and a drop in remaining capacity, are notified with the PBT_APMPOWERSTATUSCHANGE event. An app with a window receives it as WM_POWERBROADCAST; a service with no window declares SERVICE_ACCEPT_POWEREVENT and receives it as SERVICE_CONTROL_POWEREVENT in HandlerEx (WM_POWERBROADCAST does not arrive at a service control handler). On receipt, call GetSystemPowerStatus, check ACLineStatus (whether on AC) and BatteryLifePercent, and lead into interrupting measurement, saving, and requesting shutdown15
Flow of turning a power loss into a planned shutdown with a UPSWhen an outage switches the UPS to battery, PBT_APMPOWERSTATUSCHANGE is notified, power status is checked, and save plus a shutdown request turn a power loss with no notification into a planned shutdown with a notificationOutageUPS to batteryPBT_APMPOWERSTATUSCHANGEGetSystemPowerStatusInterrupt and saveRequest shutdownUsual notify flow (3–6)

A typical USB-connected UPS appears to Windows as a battery, so you can detect it with this standard API. If vendor management software has a “shut down the OS at N% remaining” feature, also check that the threshold lines up with your app’s cleanup time. Resume from sleep or hibernation, and long-running issues, are a separate axis, covered in “Sleep, Hibernation, Modern Standby, and Long-Running Apps”.

9. How to Verify — Trying Shutdown Safely

Shutdown handling tends to become “we wrote it but never tried it under production-equivalent conditions”. Keep a procedure for verifying it safely.

  • Try it on a test machine or a VM: Do not try it first on a production device PC. In a test environment with a Hyper-V checkpoint (snapshot), repeat shutdown, restart, and a forced power loss (powering the VM off). A VM “power off”, though, only reproduces “the guest OS stopping without notice”; it does not reproduce the disappearance of a physical disk’s volatile cache or controller-dependent breakage. If you ship it as a device PC, the final check is a real power-cut test on production-equivalent hardware
  • A quick check with sign-out: The WM_QUERYENDSESSION → WM_ENDSESSION path also runs on sign-out (the only difference is that the ENDSESSION_LOGOFF bit is set in lParam), so you can conveniently confirm cleanup-code behavior on a development machine1
  • Try a full shutdown and hybrid separately: Try shutdown /s /t 0 (full), shutdown /s /hybrid /t 0 (default behavior), and shutdown /r /t 0 (restart) each2
  • Measure how long cleanup takes: Write a timestamp to the log at the start and end of the cleanup function, and measure whether it fits in 5 seconds (or the configured grace period for a service)
Operations to verify and what each can confirmSign-out is a convenient check of the notification path; shutdown-command full, hybrid, and restart confirm the production notification path and grace period; VM power-off tests sudden-stop resilience; a physical power-cut test is the final check including physical storageSign-outQUERY → ENDSESSION pathshutdown /s /hybrid /rProduction path + graceVM power-offSudden guest stopPhysical power-cutStorage included (final)

For after-the-fact isolation, the event log (System) is useful. On a normal shutdown or restart, event ID 1074 (which process started shutdown, for whom, and for what reason) is recorded. On a sudden power loss or crash there is no 1074, and on the next boot event ID 41 (Kernel-Power) and 6008 (The previous system shutdown was unexpected) are recorded.14 “What happened overnight” starts here.

# Check the recent history of shutdown-related events
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
    Select-Object TimeCreated, Id, ProviderName, Message |
    Format-List

If 1074 shows “a restart by Windows Update” and the app’s data was broken, the problem is the cleanup code. 6008/41 only show “an unexpected shutdown”; they are also recorded for a blue-screen (crash) or a forced reset, not only a power loss. If 41’s BugcheckCode is non-zero it is a crash; if it is 0 and there is no memory dump either, a power loss is likely — isolate the cause from the surrounding information, and once you know it was a power loss, Chapter 8’s write design and a UPS are the next step.

10. Summary

  • Shutdown is “a normal event that will come, sooner or later”. The grace period after the notification is, in principle, only about 5 seconds, so the prerequisite is frequent autosave so that “what you do at exit” is minimized.
  • On client OSes from Windows 8 onward, if fast startup is enabled, “Shut down” is a hybrid shutdown and the kernel is only hibernating. The only full reset is “Restart” — write “Restart” into the incident procedure.
  • A GUI app returns TRUE immediately to WM_QUERYENDSESSION, and does committed cleanup in WM_ENDSESSION. WinForms/WPF FormClosing and SessionEnding correspond to the query phase, so what you do there is an idempotent snapshot save at most. Do not put up a dialog during shutdown.
  • An operation that truly cannot be interrupted is protected by displaying a reason with ShutdownBlockReasonCreate. There is, however, no guarantee anywhere that you can block.
  • A console app receives the notification with SetConsoleCtrlHandler; a service with SERVICE_ACCEPT_PRESHUTDOWN/SHUTDOWN. The default PRESHUTDOWN grace period is 10 seconds on a current OS. In .NET, stop relying on ProcessExit and move to the canonical path for the app model.
  • Recovery after a restart can be unattended with RegisterApplicationRestart (+ a recovery callback) and ARSO.
  • A power loss brings no notification. Prepare with a temporary-file + ReplaceFile swap (as a set with backup + load-time validation), a flush at checkpoints, and a UPS that “turns a power loss into a planned shutdown”.
The overall picture of shutdown handlingFor an ending with a notification, respond with cleanup that can close the shop in a few seconds and lead into automatic recovery after a restart; for a power loss with no notification, prepare with a write that does not break no matter when it is cut and a UPS, and verify including on physical hardware. These two pillars are the article's conclusionWith notificationNo notificationHow it endsFew-second cleanup (3–6)Safe write + UPS (8)Auto-recover after restart (7)Verify on hardware (9)
  • Verify safely on a VM and with sign-out, and isolate after the fact with event IDs 1074/41/6008.

The next time you add a feature to the app, ask yourself this once: if WM_ENDSESSION arrives in the middle of this work, or the power is pulled, what is left on the next launch? Writing that answer into the design is the shortest path to never spending a morning standing in front of a device PC with your head in your hands.

KomuraSoft LLC handles design and implementation of shutdown and power-loss countermeasures for device-PC and long-running apps, root-cause investigation of data corruption and “it had stopped by morning” incidents that start from a Windows Update restart or a sign-out, and design review of Windows service stop processing and automatic recovery. It is fine to start from the stage of “something seems to break every time we shut down, and I don’t know where to begin”.

References

  1. Microsoft Learn, WM_QUERYENDSESSION message. That WM_QUERYENDSESSION is sent at session end and the app should return TRUE and respect the user’s intent (DefWindowProc’s default is also TRUE); that cleanup should be deferred until WM_ENDSESSION; that after 5 seconds the system displays UI for apps that are preventing shutdown and the user can force-terminate; the meaning of the ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL bits in lParam; that shutdown and restart cannot be distinguished; and that data should be saved frequently so that less must be saved at exit.  2 3 4 5 6 7

  2. Microsoft Learn, Fast startup causes hibernation or shutdown to fail in Windows 10 or Windows 8.1. That with fast startup the kernel session is not closed and is treated as hibernation, and kernel and device-driver state are saved to hiberfil.sys; that “Restart” always does a full boot because a completely new Windows state is needed; that fast startup is enabled by default and disabling it is not recommended; and that Shutdown.exe’s default is a full shutdown, with the /hybrid option producing hybrid behavior.  2 3 4 5

  3. Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). That you call it at the start of an uninterruptible operation to register a reason string and call ShutdownBlockReasonDestroy when finished; that it can be called only from the thread that created the window; and that the user will only read the reason for a few seconds, so the string should be short and clear.  2 3

  4. Microsoft Learn, Shutdown Changes for Windows Vista. That the response to WM_QUERYENDSESSION/WM_ENDSESSION can be delayed by 5 seconds each and the user can then choose to continue or cancel; that a console app or an app with no visible window cannot abort shutdown and is terminated automatically after 5 seconds of no response or a FALSE response; that if a block is needed a reason should be registered with ShutdownBlockReasonCreate; and that an app must not depend on being able to block shutdown.  2 3 4

  5. Microsoft Learn, HandlerRoutine callback function. The CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN events received by a handler registered with SetConsoleCtrlHandler; that the default timeout for CTRL_CLOSE_EVENT is about 5000 milliseconds and for CTRL_SHUTDOWN_EVENT on a service process about 20000 milliseconds; that CTRL_LOGOFF/SHUTDOWN_EVENT are received essentially only by services because an interactive app is terminated at logoff; and that the handler runs on a separate thread.  2 3

  6. Microsoft Learn, SetConsoleCtrlHandler function. That a process that has loaded gdi32.dll or user32.dll is treated as a Windows app and the CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT handlers are not called; that the workaround is to create a hidden window and handle WM_QUERYENDSESSION/WM_ENDSESSION; and that console functions may not work correctly during signal handling.  2

  7. Microsoft Learn, .NET runtime no longer provides default termination signal handlers. That from .NET 10 the runtime no longer provides a default handler for Windows CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT (the Unix SIGTERM/SIGHUP equivalents); that the OS default handling terminates the app immediately and AppDomain.ProcessExit and AssemblyLoadContext.Unloading no longer fire; and that signal handling appropriate to the app model should be registered in a higher-level library or in app code.  2

  8. Microsoft Learn, Service Control Handler Function. That a service that declared SERVICE_ACCEPT_PRESHUTDOWN receives SERVICE_CONTROL_PRESHUTDOWN first, then a SERVICE_ACCEPT_SHUTDOWN service receives SERVICE_CONTROL_SHUTDOWN; that the default grace period at shutdown is about 20 seconds and the ceiling at OS restart is WaitToKillServiceTimeout; that this value should not be extended; that the control handler should return within 30 seconds, report STOP_PENDING and a wait hint, and leave long work to another thread; that cleanup should finish as quickly as possible with UPS operation in mind; and that the SCM at shutdown does not, by default, consider dependencies.  2 3 4 5

  9. Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). That after the PRESHUTDOWN notification the SCM waits until the service stops or the timeout; that the default timeout is 10 seconds from Windows 10 Creators Update (build 15063) onward and 3 minutes before that; that it is configured with ChangeServiceConfig2; and that status can continue to be updated during SERVICE_STOP_PENDING.  2

  10. Microsoft Learn, RegisterApplicationRestart function (winbase.h). That restart can be registered for crash, not responding, update, and computer restart accompanying an update; that command-line arguments for the restart can be specified; that registration must be done before the problem occurs and during WM_QUERYENDSESSION handling is the last chance in an update scenario; that a process under 60 seconds of runtime is not restarted; that restart after a crash or hang goes through the user’s consent; and that crossing an OS restart requires a shutdown with EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS.  2 3

  11. Microsoft Learn, Winlogon automatic restart sign-on (ARSO). That when Windows Update starts an automatic restart it saves the last interactive user’s credentials and configures Autologon; that after the restart it automatically signs the user in and locks the session; that the saved credentials are deleted after a successful sign-in; and that it can be configured with Group Policy (DisableAutomaticRestartSignOn and the like).  2

  12. Microsoft Learn, ReplaceFileW function (winbase.h). That ReplaceFile packages into a single function the multiple steps equivalent to “save to the new file, temporarily rename the original, rename the new file, delete the original”; that it preserves the original file’s attributes such as creation time, DACL, encryption, compression, and named streams; and that the backup, the file being replaced, and the replacement file must be on the same volume.  2

  13. Microsoft Learn, File Caching. That writes go onto the system cache by default and are reflected to disk by lazy writing; that FILE_FLAG_WRITE_THROUGH writes immediately to disk; that FlushFileBuffers can flush explicitly; and that file-system metadata is always cached, so confirming metadata requires a flush or write-through.  2 3

  14. Microsoft Learn, Troubleshoot unexpected reboots using system event logs. That a normal restart records event ID 1074 (which process started shutdown, for whom, and for what reason); that an unexpected restart records event ID 41 (Kernel-Power) and 6008 (the previous shutdown was unexpected); and that these IDs can isolate the kind of restart.  2

  15. Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. That this event is notified via WM_POWERBROADCAST on a switch between battery and AC or a drop in remaining capacity; and that on receipt you should call GetSystemPowerStatus and check SYSTEM_POWER_STATUS fields such as ACLineStatus, BatteryFlag, and BatteryLifePercent. 

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.

A problem that did not go away after "Shut down" went away after a "Restart". Why?
On client OSes from Windows 8 onward, when fast startup is enabled (the default on most PCs that support hibernation), "Shut down" uses a mechanism called hybrid shutdown. The user is signed out, but the kernel and driver state are saved to the hibernation file and restored as-is on the next boot. In other words, the core of the OS has not been reset. "Restart", on the other hand, always performs a full boot, so driver and service trouble is reset. Write "Restart" into your isolation procedure, not "Shut down and power back on". If you want a full shutdown from the command line, you can use shutdown /s.
Can I stop shutdown until the app finishes saving?
You can ask it to wait temporarily, but you cannot stop it reliably. If you register a reason string with ShutdownBlockReasonCreate only while an uninterruptible operation is in progress, that reason appears on the "This app is preventing shutdown" screen and the user can decide whether to continue or cancel. The user can still choose to force continuation, though, and a forced shutdown or an update-driven restart may not wait at all. The proper approach is therefore not "block", but frequent autosave so that less data is at risk, plus cleanup designed to finish in a few seconds from the exit notification.
Stopping my Windows service takes a long time. Can I extend the shutdown grace period?
In the default configuration that receives SERVICE_CONTROL_SHUTDOWN, the grace period is roughly 20 seconds and depends on the WaitToKillServiceTimeout registry value. Rewriting that value from the app side to extend it is not recommended. If you need a longer grace period, you can declare SERVICE_ACCEPT_PRESHUTDOWN and receive SERVICE_CONTROL_PRESHUTDOWN; you are notified earlier than others, and the timeout can be configured with ChangeServiceConfig2 (the default is 10 seconds from Windows 10 Creators Update onward, and 3 minutes before that). PRESHUTDOWN, however, holds up the entire shutdown for that interval, so limit it to cases you truly need, and fundamentally design the stop work itself to finish in a few seconds.
Is it safe to do shutdown cleanup in .NET's AppDomain.ProcessExit?
I recommend not relying on it. Historically the runtime registered a default signal handler, and ProcessExit fired on CTRL_CLOSE_EVENT and CTRL_SHUTDOWN_EVENT, but from .NET 10 the runtime no longer provides default termination-signal handlers, and ProcessExit no longer fires in those cases. Implement cleanup on the notification path that matches the app model: GUI apps use FormClosing or SessionEnding (those are query-phase notifications, so limit them to idempotent saves; cleanup that can run only after the session is committed belongs in a WM_ENDSESSION hook); Generic Host / Worker Service use IHostApplicationLifetime and StopAsync; console apps use SetConsoleCtrlHandler or PosixSignalRegistration.
How do I keep files from being corrupted by a sudden power loss?
Power loss brings no notification at all, so the only option is to write in a way that does not break no matter when the power is cut. The baseline is not to overwrite the original file in place: write completely to a temporary file on the same volume, flush, and swap with ReplaceFile (File.Replace in .NET). In ordinary operation this leaves you able to read either a complete old file or a complete new file, but ReplaceFile's atomicity across a power loss is not guaranteed by specification, so keep a backup (the third argument) and implement load-time recovery that validates the primary file and falls back to the backup if it is broken. Also, success from WriteFile does not mean the data has reached the disk, so at important checkpoints confirm the write with FlushFileBuffers or FILE_FLAG_WRITE_THROUGH. On device PCs the standard setup is to combine this with a UPS, detect the switch to battery, and lead into a safe shutdown.

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