ExitTimeOut appears to be capped at 60 seconds, and ExitTimeOut 0 kills immediately

On macOS 27.x (Apple silicon) I've been measuring how launchd stops a job, and two
behaviors don't match the documentation.

Method: a throwaway job whose script traps SIGTERM and logs it, bootstrapped with different
ExitTimeOut values, then stopped with launchctl bootout while timing when the process dies.

Observed (LaunchAgent in the gui domain; the same results in the system domain with a
LaunchDaemon):

  ExitTimeOut 45   -> `launchctl print` reports 45; SIGTERM, then SIGKILL at 45 s
  ExitTimeOut 61   -> reports 60; SIGTERM, then SIGKILL at 60 s
  ExitTimeOut 90   -> reports 60; SIGTERM, then SIGKILL at 60 s
  ExitTimeOut 120  -> reports 60; SIGTERM, then SIGKILL at 60 s
  ExitTimeOut 0    -> reports 0; SIGKILL after ~0.1 s, SIGTERM never delivered

man launchd.plist says of ExitTimeOut:

"The amount of time launchd waits between sending the SIGTERM signal and before sending a
SIGKILL signal when the job is to be stopped. The default value is system-defined. The
value zero is interpreted as infinity and should not be used, as it can stall system
shutdown forever."

So: is there a 60-second ceiling on ExitTimeOut? It isn't documented that I can find, and it
applies in both domains. And is zero meant to be "infinity" as documented, or an immediate
SIGKILL as observed? Finally, is there a supported way for a service to be given more than
60 seconds to exit when launchd stops it — or is 60 seconds simply the maximum?

Happy to file a Feedback with a minimal repro if that's useful.

Answered by DTS Engineer in 913482022
Written by IngmarStein in 854315021

is there a 60-second ceiling on ExitTimeOut?

Yes.

AFAICT this was introduced in macOS 26.4 (r. 47130898). This change probably should be documented in the launchd.plist man page, and I encourage you to file a bug to that effect.

Please post your bug number, just for the record.

Keep in mind that the resulting documentation might well end up being quite vague, just like it is for the current default value, because it’s easy to imagine this limit changing again in the future. This 60 second value is way too long IMO. Jobs should be able to exit much quicker than that.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Written by IngmarStein in 854315021

is there a 60-second ceiling on ExitTimeOut?

Yes.

AFAICT this was introduced in macOS 26.4 (r. 47130898). This change probably should be documented in the launchd.plist man page, and I encourage you to file a bug to that effect.

Please post your bug number, just for the record.

Keep in mind that the resulting documentation might well end up being quite vague, just like it is for the current default value, because it’s easy to imagine this limit changing again in the future. This 60 second value is way too long IMO. Jobs should be able to exit much quicker than that.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks for the confirmation, Quinn! I filed FB25376334 for the documentation.

For context: https://github.com/IngmarStein/havm/issues/27 is where one of my users ran into this problem. The job is a VZ VM where the guest can take longer than 60 seconds to shut down cleanly. I'd love to make that faster, but the guest OS doesn't support suspend / resume.

Thanks for filing FB25376334.

The obvious workaround here is to host the VM in an actual app. An app with a GUI can delay quitting indefinitely, or indeed cancel the quit. See the discussion in the applicationShouldTerminate(_:) docs.

However, there’s also an obvious downside to this, namely that the app has to show up in the Dock, app switcher, and so on.

Earlier I wrote:

Written by DTS Engineer in 913482022

This 60 second value is way too long IMO.

Think about this from a user’s perspective. They choose Apple menu > {Restart, Shut Down, Log Out} and their apps quit and then the system looks like it’s hung for 60 seconds. That’s not a great user experience.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

However, there’s also an obvious downside to this, namely that the app has to show up in the Dock, app switcher, and so on.

Yeah, that's precisely what I'm trying to avoid. The project is all about running Home Assistant as a (headless) background service, managed by Homebrew. The community around self-hosting on macOS is small and I understand it's not a priority for Apple.

Think about this from a user’s perspective. They choose Apple menu > {Restart, Shut Down, Log Out} and their apps quit and then the system looks like it’s hung for 60 seconds. That’s not a great user experience.

What I'm seeing in the Restart case is even that the VZ process gets terminated within a second an there's nothing that the host process can do about it. The resulting user experience is that after a reboot, they see an "unclean shutdown" message in the guest VM (at best) or face a data loss (at worst).

Quinn, is the maximum timeout configurable? Last time you helped me out by documenting AllowedEthernetLocalNetworkAddresses and AllowedWiFiLocalNetworkAddresses in tn3179, so I'm hoping for something similar to let users increase the cap.

Accepted Answer

I wanna start by being clear about terminology. There are three user actions in play here:

  • Log out
  • Restart
  • Shutdown

The last two are a superset of the first. That is, in order to restart or shutdown, the system must first log the user out. This thread is about the logout process, and thus applies to all three user scenarios.


Written by IngmarStein in 914173022

is the maximum timeout configurable?

No [1].

Written by IngmarStein in 914161022

What I'm seeing in the Restart case is even that the VZ process gets terminated within a second an there's nothing that the host process can do about it.

Yeah, that doesn’t surprise me. When you start a guest using Virtualization framework, the actual work is done by a suite of helper processes [2]. Once the system has progressed past the closing applications stage of the logout process, and moved on to the terminating launchd jobs, there’s nothing stopping those jobs from being terminated. And unlike your launchd agent, they’re not gonna temporarily ignore the SIGTERM.

There is another approach you might take here, namely to create a UI element. This uses an app-like structure but it has the LSUIElement property set in its Info.plist and thus doesn’t appear in the Dock. However, it can present UI. Commonly this involves a status item on the menu bar, which seems like a good match for what you’re doing.

You can configure your UI element as a login item rather than a launchd agent. This makes it behave more like an app.

UI elements can’t participate in the applicationShouldTerminate(_:) dance, but IIRC they are terminated by loginwindow, rather than launchd, and thus are part of the closing applications phase. That might help in this situation, but after researching this I’m not super confident that it will.

Note If you want to gain insight into how loginwindow orchestrates the logout process, monitor the com.apple.loginwindow.logging subsystem, and specifically its Standard category. For lots of hints and tips on how to use the system log, see Your Friend the System Log.

Beyond that, I don’t have any great suggestions. There’s a clear tension here. Apple has done a lot of work to make sure that macOS shut downs quickly. The only thing that can defer logout for long periods of time is a GUI app, and that’s something you’re trying to avoid.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] If I’m reading the code correctly, there is a way to customise this, but a) it’s not documented, and b) it requires you to configure a bootarg, which means disabling SIP, which isn’t a path you, or I, want to be heading down.

[2] The exact mechanics of this is an implementation detail, but if you rummage around in Activity Monitor you can see how it works today.

Thanks once again, Quinn!

To close the loop: I ended up transforming the LaunchAgent into a regular application with a Dock tile to prevent it from being killed outright during the logout orchestration. I even found some useful operations to be exposed via the tile, so it's not just a dummy UI. My upstream fixes to Home Assistant OS to speed up shutdown (https://github.com/home-assistant/supervisor/pull/7448) were also merged, so that the next release has higher chances of fitting into the 60 second budget. Until the man page is updated, this forum post serves as documentation for the ExitTimeout cap.