I’ve been running this on a production HestiaCP server for a while and wanted to share it — it solves a gap in HestiaCP’s Resource Limits.
The problem. HestiaCP enforces per-account limits through systemd user-<uid>.slice cgroups, but only processes spawned via an interactive session (login/SSH/cron/CLI) land there automatically. php-fpm workers and Dovecot session processes (IMAP/POP3/managesieve) never do — they stay in the service slices and escape the account’s limits entirely.
What it does. A small native Rust daemon that runs alongside HestiaCP and:
recreates missinguser-<uid>.slice slices after reboot (writing the user’s HestiaCP CPU_QUOTA/MEMORY_LIMIT limits into the cgroup)
detects php-fpm workers and Dovecot session processes, resolves their owner’s package, and moves them into the account’s slice
applies optional cgroup v2 attributes per package (memory.high, cpu.weight) that HestiaCP doesn’t expose
Exim addon: a small Exim ACL rule + notifier that, after SMTP AUTH, moves the authenticated SMTP connection process into the sender’s slice too (verified authenticated_id → uid → slice)
Why Rust. No ps, grep, awk or jq runs in the loop — it talks to /proc and cgroupfs directly, so it’s cheap on busy servers.
Safety design. HestiaCP stays the owner of cgroups — the daemon never touches an existing slice, never replaces Resource Limits, and every Exim submission is verified against /proc/<pid>/exe before anything is moved. 61 unit tests, MIT licensed, nothing to install on clients.
Install:
git clone https://github.com/hedon77/process-slice-manager.git
cd process-slice-manager
sudo bash install.sh --with-exim # --with-exim adds the SMTP AUTH notifier
Yes. HestiaCP supports Debian 12 (bookworm), and the daemon only depends on paths that Debian 12 provides:
/usr/local/hestia/data/users and /usr/local/hestia/data/packages — identical on Debian
cgroup v2 — Debian 12’s systemd uses the unified hierarchy by default (verify: [ -f /sys/fs/cgroup/cgroup.controllers ])
The install.sh bootstraps Rust itself if cargo is missing
One thing to be aware of: the optional Exim addon patches /etc/exim4/exim4.conf.template. On Debian 12 with HestiaCP’s Exim 4.96 the template is the same monolithic one, and the patch is idempotent with an hourly self-healing timer, so it recovers on its own if an upgrade resets it. I tested on Ubuntu 24.04 (Hestia 1.9.8, Exim 4.97); I’d be glad to hear a Debian 12 confirmation from anyone who tries it.
Q2: “Does php-fpm normally limit processes by load?”
No — that’s exactly the gap this daemon fills. php-fpm’s own pm settings (pm.max_children, pm.start_servers, etc.) only cap the number of workers and how many PHP requests run concurrently. They do not limit CPU or memory per account.
HestiaCP’s Resource Limits (CPUQuota/MemoryMax) are enforced through systemd user-.slice cgroups. But those slices only catch processes that land there automatically — interactive sessions (login/SSH/cron/CLI). php-fpm workers are spawned by the php-fpm master inside /system.slice/php8.x-fpm.service and never inherit the account slice, so they bypass the panel’s limits entirely. The daemon detects each worker’s real uid, resolves the owner’s package, and moves the worker into user-.slice, where the account’s CPU/memory limits then actually apply.
Q3: “The first version of the module was quite crooked…”
Fair — this is a complete rewrite in Rust, not a patched bash script. The old bash daemon is gone; the Rust version:
has no ps/grep/awk/jq in the monitor loop — it reads /proc and cgroupfs directly
never touches an existing slice (HestiaCP stays the owner of cgroups) and never replaces Resource Limits
verifies every Exim submission against /proc//exe before moving anything
Every <name>.pkg file there is a package (the .sh files next to them are Hestia’s own per-package hooks and are ignored). The daemon also reads each user’s user.conf to map user → package. So the panel stays the single source of truth: add/rename a package in HestiaCP and the daemon picks it up on its next reload (which happens automatically when a user is added or a package changes, via the installed <Package>.sh hooks → SIGHUP).
The [Package] sections in the config file are entirely optional and only exist if you want to apply extra cgroup v2 attributes that HestiaCP doesn’t expose (e.g. memory.high, cpu.weight, io.weight). Rules:
an attribute is applied only if it’s listed in the config
attributes not listed are never touched — HestiaCP’s own limits stay intact
a package with no[Package] section simply gets no extensions — the core behaviour (moving php-fpm/Dovecot/Exim processes into the account’s user-<uid>.slice) still works for it, because that uses the limits HestiaCP already set in user.conf
The example file ships with a [default] section plus Mini/Pro/Business — those are just examples. You can delete them all and the daemon runs fine; you only add sections for the packages where you want extra attributes.
Thanks for the feedback — I’ve added uninstall.sh to the repo.
sudo bash uninstall.sh
It removes everything install.sh put in place, and it does handle the Exim patch the way you asked:
stops and disables the psm-exim-patch.timer self-healing timer first, so it can’t re-inject the rule while we’re uninstalling,
strips the injected warn rule from /etc/exim4/exim4.conf.template,
runs update-exim4.conf and reloads Exim, so the rule disappears from the generated /var/lib/exim4/config.autogenerated too,
deletes the notifier and the patch script,
then removes the daemon binary, config, systemd unit, logrotate config, the HestiaCP per-package hooks and the psm-monitor.sh viewer.
HestiaCP’s own user-<uid>.slice cgroups and the daemon log are left untouched.
On php-fpm load limiting vs. codebyoul/php — both projects cap php-fpm CPU/RAM, but they take opposite approaches, and the overhead is a big part of the difference:
codebyoul/php is the bash daemon. It builds its own cgroup tree (/sys/fs/cgroup/<Package>/tasks/), writes CPU/memory/swap limits into it from user.conf, and moves every non-root process of a user there. It runs a second, parallel limit hierarchy the panel doesn’t know about. And on every 2 s cycle it shells out to v-list-users json, pipes it through jq for every user and package, and parses a full ps listing — that’s the real overhead: a bunch of forked processes and JSON/text parsing just to decide which cgroup a pid goes into.
process-slice-manager is the Rust rewrite. It reuses the user-<uid>.slice cgroups HestiaCP already manages and only moves php-fpm workers, Dovecot session processes and (opt-in) Exim SMTP AUTH connections into them. It never writes a limit itself — it makes those processes count against the CPUQuota/MemoryMax already configured in the panel. The monitor loop reads /proc and cgroupfs directly — no v-list-users, no jq, no ps, no awk — so there is no per-cycle parsing overhead at all. Single static binary, one systemd unit.
So: yes, it still limits php-fpm load — the workers land in the account slice where HestiaCP’s limits apply (the gap the bash version also targeted) — but the enforcement lives in the panel’s own hierarchy, and the per-cycle cost that made the bash version heavy on busy servers is gone.