Let systemd-oomd kill a runaway app instead of losing the session (#6445)

Nothing stood between "memory is tight" and "processes die at random":
the kernel OOM killer only fires after an allocation has already failed,
so a machine under pressure thrashes in reclaim until something breaks
somewhere fatal — seen in practice as Hyprland taking SIGBUS mid-memcpy
from a client shm pool that memory pressure was tearing down.

Enable systemd-oomd, which keys on PSI stall time rather than free
pages, and mark app.slice — and only app.slice — as its kill candidate.
Hyprland runs in session.slice, so the compositor is structurally
ineligible as a victim: oomd takes the app that caused the pressure and
the session survives. Thresholds (50% stall over 20s, Fedora's desktop
defaults) live in an oomd.conf.d drop-in; candidacy ships as a vendor
drop-in under /usr/lib/systemd/user so existing users get it on package
upgrade with no per-user seeding.

New installs enable the daemon from enable-services.sh; a migration
covers existing ones, restarting an already-running oomd so it doesn't
keep stale thresholds until reboot.

Deliberately not earlyoom: it triggers only when free RAM and free swap
are both under threshold, so Omarchy's large, mostly-idle swapfiles
keep it asleep through exactly this failure.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-31 15:16:29 -04:00
committed by GitHub
co-authored by Claude Fable 5
parent 5844e02d2e
commit 1ea910f662
6 changed files with 104 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
echo "Let systemd-oomd kill a runaway app instead of the whole session"
# New installs get this from install/config/enable-services.sh. Existing ones
# have never had an OOM daemon, so nothing stands between memory pressure and
# the session falling over.
as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}
# Machine-wide, so a second user on the same box finds it already done.
if systemctl is-enabled --quiet systemd-oomd.service 2>/dev/null; then
# Already enabled means it started before the package delivered our
# thresholds in /etc/systemd/oomd.conf.d/, and oomd only reads that at
# startup. Restart so it doesn't run with stale limits until reboot.
as_root systemctl try-restart systemd-oomd.service >/dev/null 2>&1 || true
else
as_root systemctl enable --now systemd-oomd.service >/dev/null 2>&1 ||
echo "Could not enable systemd-oomd.service; memory pressure will still take the session down."
fi
# Pick up /usr/lib/systemd/user/app.slice.d/10-oomd.conf without waiting for
# the next login. That drop-in is what marks app.slice as a kill candidate;
# until the user manager reloads and reports it, oomd is running with nothing
# to act on. An `omarchy update` over SSH or from a TTY has no user manager to
# reload, and there the next graphical login picks it up on its own.
systemctl --user daemon-reload >/dev/null 2>&1 || true