shutdown
Edit on GitHubModule: modules/nixos/core/shutdown.nix
How long systemd waits for a unit to stop before the SIGKILL.
The symptom, and the number that gave it away
"A stop job is running…" and the shutdown taking a minute and a half. Measured on 09/08/2026 over the last 10 boots: 90.3 / 90.4 / 90.5 / 90.6 s. Far too round to be real work. That is not the system doing something, it is a TIMEOUT firing, and the timeout was systemd's default: 90 s.
A single culprit, on the user's side
VS Code. It runs in a session scope (app-code-<pid>.scope, created by GLib when launching
the .desktop) and it does not answer the SIGTERM. Every shutdown closed the same way:
app-code-*.scope: Stopping timed out. Killing.Before VS Code, the same pattern showed up with Chromium. It is Electron behavior, not this machine's. EVERYTHING else (docker, jellyfin, the network, the unmounts, swap) stops in under 2 s, so there was nothing to optimize, only waiting on a process that was never going to answer.
What the change does not do
It does not start killing anything that used to die a natural death. The SIGKILL already happened, 90 s later. Whoever saves state on SIGTERM (pipewire, dropbox, rclone, all the daemons) takes under 1 s and keeps saving; whoever ignores the signal merely stops charging us the wait.
Why 5 s on the user side and 30 s on the system side
The two sides have different tenants, and a single value would serve both badly.
- User: desktop apps. Whoever was going to save has saved; what is left is a hung Electron. 5 s is generous slack for an honest SIGTERM.
- System:
docker compose downsits in the ExecStop of duo and grad-radar, anddowngives EACH container 10 s of grace before killing it. A tight ceiling here would SIGKILL those stacks' Postgres in the middle of the down. It does not corrupt anything, but it comes back doing WAL recovery on the next boot, and the price shows up far from the cause. 30 s covers the down with room to spare and is still 3x faster than the default.
It is a default, not a ceiling
A unit with its OWN TimeoutStopSec ignores everything here. Today that is qbittorrent (30 min),
jellyfin (15 s), caddy (5 s), hyprpolkitagent (5 s) and user@.service (2 min, from upstream). If
the shutdown ever gets slow again, look first for whoever declared their own value:
systemctl show <unit> -p TimeoutStopUSecThe two sides have different APIs, and the asymmetry is a real trap
systemd.extraConfig WAS REMOVED (26.05 wants systemd.settings.Manager, freeform), but
systemd.user.extraConfig is still the only form on the user side, because systemd.user.settings
does NOT exist (checked in the options).
The way that fails is the worst possible: writing to the removed option, the nix eval of the
generated system.conf passes and comes out WITHOUT the line. Nothing warns you. So the validation
is not "did it build?", it is READING the generated file:
nix eval --raw '.#nixosConfigurations.ex-b560m-v5.config.environment.etc."systemd/system.conf".text'To check the effect without a stopwatch, the journal's two stamps:
journalctl -b -1 -o short-precise | grep -E "Stopping User Manager|Journal stopped"Measured again: 17 s down to 7 s (02/11/2026)
With the splash, the clock runs from Show Plymouth Reboot Screen (or Power Off) to Journal stopped. Six shutdowns from 27/10 to 02/11 took 16.6 to 18.1 s, and 10 s of that was one
container:
grad-radar-backend-1: Container failed to exit within 10s of signal 15 - using the forceIts compose command was sh -c "uv sync && uvicorn ...", so PID 1 was sh, which never forwards
SIGTERM, and down waited out Docker's 10 s grace every time. init: true would not have helped:
tini forwards to its child, which is still sh. The fix is exec uvicorn in the grad-radar repo
(684b1fd): compose stop backend went from 10.2 s and exit 137 to 0.55 s and exit 0.
The three shutdowns after it with the stack running took 6.8, 7.0 and 7.7 s, no forced kill. What is left is the Docker daemon waiting on Home Assistant, which saves its state on the way out (about 5 s); no timeout was changed for it. To repeat the measurement on the last boot:
journalctl -b -1 -o short-precise | grep -E "Plymouth (Reboot|Power Off) Screen|using the force|Journal stopped"hyprshutdown: looked at, not adopted (22/10/2026)
hyprshutdown 0.1.1 (in the pinned
nixpkgs and in unstable) opens a dialog, asks every window to close, then exits Hyprland; powering
off is a --post-cmd that runs AFTER Hyprland is gone. Against what runs here:
- The autologin races it. LightDM logs this session back in as soon as Hyprland exits, so a
--post-cmd systemctl poweroffwould compete with a fresh session starting. The current path asks logind first, and logind owns the order of the stop. - It adds nothing to reboot and poweroff. systemd already sends SIGTERM to every app and waits up to the 5 s above; an app that saves on a close request but not on SIGTERM is the only gain, and none is known here.
- Log out is where it could help: a graceful close instead of
loginctl terminate-user. That needs the autologin race above tested first. Dropped on 02/11/2026: no app here is known to lose anything onterminate-user.
So the session's actions stay on systemd and logind, and the NixOS Center's confirmation (nixos-center) asks first.