Run a heavy command inside a memory-capped systemd user scope, so that when it blows past available RAM the kernel OOM-kills the command — not your whole machine, and not (under WSL2) the entire Linux VM.
heavy-run Rscript analysis/run_pipeline.R
HEAVY_RUN_MEM=10G HEAVY_RUN_SWAP=2G heavy-run python train.pyFork-parallel workloads — R mclapply / BiocParallel, Python multiprocessing.Pool / joblib — default to all cores and each worker inherits the parent's heap. Peak memory is closer to parent_RSS × (1 + N_workers) than anyone expects, and when it exceeds physical RAM the result isn't a clean error: the Linux OOM killer thrashes, and on WSL2 the whole VM can lock up or die (wsl.exe exits, broken pipe, "catastrophic failure").
heavy-run puts the command in its own cgroup with a hard MemoryMax. If the job exceeds the cap, only the job dies — with a normal non-zero exit — while the rest of the system stays responsive.
heavy-run <command> [args...]
| Env var | Meaning | Default |
|---|---|---|
HEAVY_RUN_MEM |
MemoryMax for the scope |
14G |
HEAVY_RUN_SWAP |
MemorySwapMax for the scope |
4G |
HEAVY_RUN_RESERVE |
GiB of MemAvailable to keep free; refuse to launch if the cap would dip below it (0 disables) |
2 |
HEAVY_RUN_FORCE=1 |
Skip the headroom pre-flight entirely | — |
Sizes accept K/M/G suffixes (e.g. 10G, 512M).
Before launching, heavy-run reads MemAvailable from /proc/meminfo and refuses if granting the requested MemoryMax would leave less than HEAVY_RUN_RESERVE GiB free — so you find out now, not after a doomed job churns for an hour and gets killed anyway. Override with HEAVY_RUN_FORCE=1.
- User-mode systemd with the memory controller delegated to the user slice. This is the default on modern systemd Linux, and on WSL2 with
systemd=truein/etc/wsl.conf. - If
systemd-runisn't found,heavy-runrefuses (exit 69) — a safety wrapper that silently runs uncapped is worse than none. To run anyway, say so:HEAVY_RUN_UNCAPPED=1 heavy-run …. (Before 1.1.0 it fell through to an uncapped run with only a warning.) - Note
ulimit -v/RLIMIT_ASis not a substitute: it caps each process's address space, not the aggregate of a fork-heavy job — exactly the workload this tool exists for.
Verify your setup:
systemd-run --user --scope --quiet -p MemoryMax=1G -- true && echo "delegation OK"curl -fsSL https://raw.githubusercontent.com/musharna/heavy-run/main/heavy-run -o ~/.local/bin/heavy-run
chmod +x ~/.local/bin/heavy-run(Ensure ~/.local/bin is on your PATH.)
Review the script before installing — it sets cgroup memory limits and execs arbitrary commands you pass to it.
It's a single ~90-line bash script. The core is one line:
exec systemd-run --user --scope --quiet --same-dir \
-p MemoryMax="$MEM" -p MemorySwapMax="$SWAP" -- "$@"--scope runs the command synchronously in the foreground (you keep stdout/stderr and the exit code); --same-dir preserves your working directory; the -p properties set the cgroup memory limits.
MIT — see LICENSE.