Shutdown handling of processes
Shutting down a multi-process, multi-threaded application is not an easy task. We want to achieve different, partly conflicting goals:
Be fast and non-disturbing for the user
Ensure that important things have been accomplished (like sending telemetry, closing files in a non-corrupting way, …)
Free all resources in a predictable way (at least in leak-checking builds)
So we cannot just _exit(0)
at any point in time, regardless the inner status we have.
Note: However we must always be aware that processes can be killed anytime for reasons that are out of our control and thus we should minimize the risk to break anything if we were not able to do an orderly shutdown. This is particularly true on Android where it is very likely to be just killed randomly by the system to reduce resource usage.
Shutdown flavors
Main process Shutdown
If the browser is asked to close by the user or by other external events, the main process needs to orchestrate the shutdown of all child processes and itself. This shutdown is organized in phases to allow us to manage dependencies, to some extent.
Note: As many things in Gecko, shutdown is managed via notification observers. But in order to have a stricter model of subsequent shutdown phases that must happen in a certain order after passing the point of no return,
AppShutdown
andShutdownPhase
provide a stricter wrapper around the pre-existing notification topics. Only topics that have a correspondingShutdownPhase
are considered to be relevant and only those phases can be used with shutdown related functions asClear/RunOnShutdown
orAsyncShutdown
.
ShutdownPhase | Notification Topic | Notes |
---|---|---|
n/a | "quit-application-granted" |
We passed the point of no return. Observers of this notification should be aware, that we are sure to shutdown, but did not yet initialize the shutdown supporting infrastructure to detect hangs (that is: nsTerminator , see below). |
n/a | "quit-application-forced" |
We passed the point of no return. This is currently only used by nsAppStartup itself and can and should be normally ignored. |
AppShutdownConfirmed |
"quit-application" |
We initialized our hang monitoring. Breaks existing spinEventLoopUntilOrQuit loops (and also remaining modal dialogs). Some chrome code shutdown blockers react on this. Interrupts content processes' content JS execution. |
AppShutdownNetTeardown |
"profile-change-net-teardown" |
The network is going away. |
AppShutdownTeardown |
"profile-change-teardown" |
Shut down some components like preallocated processes manager, GMP, places, … |
AppShutdown |
"profile-before-change" |
All content processes are shut down. A lot of remaining application services and components shut down. |
AppShutdownQM |
"profile-before-change-qm" |
Shutdown the quota manager and its clients. Can be IO heavy and is thus hang prone. |
AppShutdownTelemetry |
"profile-before-change-telemetry" |
Send last telemetry and shut down the service. |
XPCOMWillShutdown |
"xpcom-will-shutdown" |
Only a few listeners. From this point we are inside ShutdownXPCOM . |
XPCOMShutdown |
"xpcom-shutdown" |
The last phase in which we can do things in a sane XPCOM environment. Quite a few components register a shutdown blocker here. |
XPCOMShutdownThreads |
"xpcom-shutdown-threads" |
Shutdown and join all nsThread based threads (and pools) except the main thread, as well as the timer thread and servo thread pools. Once we get here we are already in a partly dysfunctional state, things that shutdown here should not depend on many other things working for them to shutdown cleanly. Ideally the threads are idling already when we get here. |
XPCOMShutdownFinal |
n/a | Main thread shutdown, last round of main thread events execution. The observer service is already dead at this stage. |
CCPostLastCycleCollection |
n/a | We destroy the main thread's JS Context and unload static XPCOM components. |
Note: There is a dance of other notifications and checks before we really enter shutdown in order to determine, if we can actually shutdown, in particular to manage
beforeunload
event handlers in content processes. This is out of scope here.
Fast Shutdown
In opt builds, the shutdown sequence can finish earlier depending on the toolkit.shutdown.fastShutdownStage
preference.
Stage | Bail out before Phase |
---|---|
0 | none, normal shutdown |
1 | CCPostLastCycleCollection |
2 | XPCOMShutdownThreads |
3 | XPCOMShutdown |
The basic idea is that in the late shutdown phases in theory nothing important happens other than freeing process resources that will be freed easier and faster by the OS on _exit(0)
. As of July 2025, this is 1
in release builds, and there is no active effort to shift shutdown earlier.
ProcessChild Shutdown
Single child processes are created and closed during a normal browsing session frequently. This is particularly true for content processes. Shutting down a single child process should not block the user while interacting with the application.
In order to make the child process aware as early as possibly that it is meant to go away, IPC allows to send the IMPENDING_SHUTDOWN_MESSAGE_TYPE
via MessageChannel::NotifyImpendingShutdown()
which is elaborated directly on the IPC I/O child
thread without passing through the main thread. It basically sets a global, atomic flag that can be checked via AppShutdown::IsShutdownImpending()
.
As of July 2025, this is used for ContentChild
derived processes to signal them to interrupt content JS processing to ensure they can timely process the shutdown events on the main thread and for some other child-only shutdown checks.
Note: Calling
AppShutdown::IsShutdownImpending()
in other process types than content is basically equivalent with checkingAppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)
(though it is actually flipped before"quit-application-granted"
is notified in the parent). It can always be used as the earliest sure indicator of shutdown in any process type.
Of the above shutdown phases, child processes are only aware of:
ShutdownPhase | Notification Topic | Notes |
---|---|---|
AppShutdownConfirmed |
"content-child-will-shutdown" |
Only in content processes, see the notification topic. We passed the point of no return. Will break existing spinEventLoopUntilOrQuit loops. |
XPCOMWillShutdown |
"xpcom-will-shutdown" |
Same as parent. Note that in non-leak-checking builds, content processes exit before entering XPCOM shutdown at all! |
XPCOMShutdown |
"xpcom-shutdown" |
Same as parent. |
XPCOMShutdownThreads |
"xpcom-shutdown-threads" |
Same as parent. |
XPCOMShutdownFinal |
n/a | Same as parent. |
CCPostLastCycleCollection |
n/a | Same as parent. |
Make your Code Shutdown aware
As of July 2025, we do not have a standardized way to manage dependencies of lazily constructed components that we could leverage on shutdown. Instead components must be shut down in the correct phase.
Note: For the ease of discussion we use the term “component” in a very broad way here. It may be any of component, singleton, service like in “something that wants to do something at shutdown”. Often this is something with explicit initialization via a lazy getter or similar.
There are a few things you can and most likely should consider to avoid later surprises:
Assuming your component is created lazily: What is the latest shutdown phase when it should be created?
When should it be torn down and/or stop serving clients?
When should it be entirely destroyed?
Late Initialization and/or Resurrection Checks
A lazily instantiated component should always check, if it is still the case to get instantiated. In C++ you can do this like this:
if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
}
The example uses an early phase on purpose, as most likely you want to avoid the useless creation of a component as early as possible.
Note: This check works from any thread, as opposed to
PastShutdownPhase
which needs to run from the main thread.
In chrome JS instead the equivalent check would look like this:
if (
Services.startup.isInOrBeyondShutdownPhase(
Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
)
) {
// reject some promise or similar...
return;
}
Note: In JS, each
await AsyncFooService.ready();
or similar inside an initializing routine may open up a can of worms if that other service is not shutdown aware and able to signal an error. Please check your callees for their behavior when hardening against resurrection.
Selfmade Shutdown Handler: Observe a Shutdown Topic
You can simply register for one of the shutdown topics and react on it. Be aware that this approach has the disadvantage that observer notifications are synchronously executed one by one. Any heavy processing or potentially blocking I/O made here would block the main thread - unless you start adding SpinEventLoopUntil
calls on your own, which have their own caveats.
Note: The order of execution within one phase is the inverse order of registration. It is good practice to not rely on this but choose different phases to manage dependencies.
This is considered to be a legacy approach. There might be still valid reasons to do this, but first look at the other methods, please.
Simple Shutdown Handler for C++: RunOnShutdown and ClearOnShutdown
You can run a lambda or other callable simply with:
#include "mozilla/ClearOnShutdown.h"
...
RunOnShutdown([] {
// Do something on the main thread.
}, ShutdownPhase::<SomePhase>);
This has basically the same effect as a registered observer for a shutdown topic. Again it is run synchronously and should not do any heavy or async things.
This method is mostly useful for simple cleanups that can and should run late, thus the default phase of ShutdownPhase::XPCOMShutdownFinal
.
You can also just pass a reference to any (static) SmartPtr
to ClearOnShutdown
. It will be cleared during the given shutdown phase, the default is again ShutdownPhase::XPCOMShutdownFinal
, always on the main thread like for RunOnShutdown
.
Note: The order of execution within one phase is the inverse order of registration (mixed between
ClearOnShutdown
andRunOnShutdown
). It is good practice to not rely on this but to choose different phases to manage dependencies. The entire list is executed before the observer notification is fired.
Full blown Shutdown Handler: AsyncShutdown Blockers
They are easy to use from JS but also helpful in C++ (yet a bit verbose). The main advantage is the async, promise-based execution, that is while they are still executed on the main thread, when a blocker waits for async things (like something running on a different thread), the other blockers of the same phase continue to run, hopefully widening the sequential bottleneck of the main thread.
Be aware that (as of this writing) AsyncShutdown
is implemented in JS and can only be used in parent and content processes.
Warning: You cannot rely on any order of execution between different blockers running inside the same phase.
See AsyncShutdown for a general introduction.
Predefined barriers:
ShutdownPhase | Barrier | Processes |
---|---|---|
AppShutdownConfirmed |
appShutdownConfirmed |
Parent only. |
AppShutdownTeardown |
profileChangeTeardown |
Parent only. |
AppShutdown |
profileBeforeChange |
Parent only. |
AppShutdownTelemetry |
sendTelemetry |
Parent only. |
XPCOMWillShutdown |
xpcomWillShutdown |
Parent & Content. Note that in non-leak-checking builds, content processes exit before entering XPCOM shutdown at all! |
Note: The confusing names of the
profile*
barriers have legacy reasons, stemming from old notification topic names that lost most of their meaning during time. They might eventually change to better reflect the shutdown phases.
If you wish to have more predefined barriers, please make sure that they are mapping to existing shutdown phases. And be aware that the current JS implementation of AsyncShutdown
does not work well with later XPCOM phases.
C++: Consider splitting the Shutdown
You might want a component to stop requests to be served and close all in-flight work relatively early, for example during AppShutdownConfirmed
. On the other hand it is not necessary to free all its resources such early, the later you do so the more likely you might benefit from Fast Shutdown.
A strategy is to intercept an early shutdown phase with one of the above methods, do whatever it needs to deactivate the component, and delegate the boring rest of the cleanup to a later phase (via RunOnShutdown
or ClearOnShutdown
). Note that JS code usually does not need to bother about memory cleanups, of course.
Test harness special cases
Be aware that not all test harnesses leverage all shutdown phases. gtests are not supposed to use any of them for the main process (but in launched child/browser processes yes), xpcshell-tests do not go through all of them. Other test harnesses might run a random selection of tests before shutting down the parent process, so you cannot know if a shutdown is going to happen after your test. This might lead to missing test coverage if whatever shutdown behavior you want to be tested implicitly is never run or in moments you don’t expect. Due to this many shutdown code paths are rarely tested intentionally and problems pop up randomly, either on CI or in the wild.
Shutdown Hang Detection
As we execute a lot of code on multiple threads during shutdown, there are many things that can go wrong and cause us to hang. Be it for high CPU load, life- or deadlocks, blocking IO or other reasons, shutdown hangs are quite easily caused when adding shutdown blocking code. We thus have some instrumentation to detect and examine those hangs.
Main Process Hangs
There are two standard ways of detecting shutdown hangs in the parent.
AsyncShutdown Blocker Timeout Crash
The AsyncShutdown
handler will timeout-crash after toolkit.asyncshutdown.crash_timeout
ms, currently 60s by default. The timeout is reset on the start of every phase.
The resulting crash reports will carry AsyncShutdownTimeout
in their signature, plus a hint on where the hang is located. In its annotations you can find AsyncShutdownTimeout
with details on the blocker that caused the hang.
nsTerminator Timeout Crash
The separate nsTerminator
watchdog is initialized before entering ShutdownPhase::AppShutdownConfirmed
and for each phase it resets a shutdown timeout of toolkit.asyncshutdown.crash_timeout + 3000
ms, that is currently 63s. It captures all things ongoing between the notification of two shutdown phases, not only AsyncShutdown
blockers, in particular everything synchronous happening in response to observer notifications. It is active for all shutdown phases, also the latest that AsyncShutdown
cannot cover.
The resulting crash reports carry shutdownhang
in their signature, with a crash reason Shutdown hanging at step <shutdown phase>. Something is blocking the main-thread.
. The XPCOMSpinEventLoopStack
annotation might give hints on where we’re waiting, in case the main thread stack is truncated.
Tip::
nsThread
names are modified during their shutdown with the following markers:SHDRCV
once the thread is aware to be asked to shutdown andSHDACK
once the thread finished its shutdown and sent the ack back to the parent thread. In case of hangs inXPCOMShutdownThreads
this can be helpful to see in the crash report, which threads are trying to shutdown. A common cause for threads not joining timely is the parent (most of the times: main) thread being too busy to ever process the acknowledgement.
Content Process Hangs
Content processes are force killed by the parent after dom.ipc.tabs.shutdownTimeoutSecs
if they do not shutdown timely (by default within 20s). In this case the parent continues to run and only the child is killed.
Only in nightly builds this will cause the generation of a so-called “paired minidump”. The resulting crash report carries ShutDownKill
in the signature. The stacks visible in crash-stats refer to the child process, but in the “Raw data and minidumps” tab authorized users can download the minidump of both the child and the parent process in the moment of hanging.