Skip to content
← ENGINEERING NOTES
LINUX7 min read

Cap CPU and RAM with cgroup v2 and systemd-run

Learn how Linux cgroup v2 limits CPU, memory and process counts, and use systemd-run to cap a runaway job in one command, with the raw files to inspect.

BY ASHLIN DARIUS GOVINDASAMY

We all have that one job: the build, the data import or the experimental script that quietly eats every core and every gigabyte until your desktop freezes and you reach for the power button. Linux cgroup v2 limits exist so that never has to happen, and with systemd-run you can cap CPU and memory for any command in a single line. This guide shows the one-liner, then the raw cgroup files underneath so you understand what it is actually doing.

What a cgroup actually is

A control group is a node in a tree that the kernel exposes as a filesystem, normally mounted at /sys/fs/cgroup. Every process belongs to exactly one cgroup, and controllers such as cpu, memory, io and pids attach limits and accounting to that node. In cgroup v2 there is a single unified hierarchy, so one tree describes everything, unlike v1 where each controller had its own tree. Modern systemd distributions use v2 by default.

bash
stat -fc %T /sys/fs/cgroup     # prints cgroup2fs on a v2 system
cat /proc/self/cgroup          # e.g. 0::/user.slice/user-1000.slice/session-3.scope
cat /sys/fs/cgroup/cgroup.controllers
Check that you are on cgroup v2 and see where your shell lives

The four knobs worth knowing

The kernel's cgroup v2 admin guide documents dozens of files. In daily life four families matter. The naming pattern is helpful: min and max are hard guarantees and limits, low and high are soft ones, and weight is a proportional share.

  • cpu.max takes two numbers, a quota and a period in microseconds. The value 50000 100000 means at most 50 ms of CPU time every 100 ms, so half of one core. The word max in place of the quota removes the cap.
  • cpu.weight is a relative share with a default of 100 and a range of 1 to 10000. It only matters when CPUs are contended, so it never wastes idle capacity.
  • memory.high is the soft limit. When a group goes over it, the kernel throttles its allocations and reclaims memory instead of killing anything. memory.max is the hard limit, and exceeding it triggers the OOM killer inside that group.
  • pids.max caps how many tasks the group can contain, which is the classic defence against a fork bomb.

The one-liner: systemd-run

You rarely need to write to /sys/fs/cgroup by hand, because systemd owns the tree and exposes the knobs as unit properties. CPUQuota= maps onto cpu.max, MemoryHigh= and MemoryMax= map onto the memory files, and TasksMax= onto pids.max, as described on the systemd.resource-control manual page. The systemd-run command wraps any program in a transient unit with those properties.

bash
sudo systemd-run --scope \
  -p CPUQuota=50% \
  -p MemoryHigh=900M \
  -p MemoryMax=1G \
  -p TasksMax=200 \
  make -j"$(nproc)"
Run a command with a ceiling on CPU, memory and tasks

The --scope flag runs the command in the foreground as a transient scope unit, so your terminal, output and exit status behave as usual. Without it, systemd-run starts a background service and returns immediately. Add --unit=my-build if you want a stable name to inspect afterwards.

QUICK CHECK

A process with CPUQuota=50% is running on an otherwise idle 8-core machine. What is the most CPU time it can use?

Watch it happen in the raw files

Let us prove the limit is real by creating a memory hog and reading the cgroup files while it runs. Naming the unit makes the path predictable. A transient scope created by systemd-run lands under system.slice for system-level units.

bash
sudo systemd-run --scope --unit=hog \
  -p MemoryHigh=200M -p MemoryMax=300M \
  python3 -c "
import time
chunks = []
while True:
    chunks.append(bytearray(10 * 1024 * 1024))
    time.sleep(0.05)
"

# In a second terminal:
G=/sys/fs/cgroup/system.slice/hog.scope
cat $G/memory.current $G/memory.high $G/memory.max
cat $G/memory.events          # high and oom_kill counters climb
systemctl status hog.scope
A memory hog that gets throttled, then killed

You should see memory.current climb toward 200M, flatten as throttling starts, and the high counter in memory.events increase. If the process still pushes through, it eventually crosses 300M and the oom_kill counter ticks up. The kernel killed only that scope, not your browser. Note that if swap is available the hog may spill into it first, so add -p MemorySwapMax=0 for a clean demonstration.

›Deep dive: why you cannot write limits on any cgroup you like

cgroup v2 enforces the no internal processes rule: a cgroup that has controllers enabled for its children, through cgroup.subtree_control, cannot itself contain processes (with exceptions for the root). Controllers are enabled top-down, so a child only gets the memory or cpu files if its parent listed that controller in cgroup.subtree_control. This is why delegating to unprivileged users is a systemd feature (Delegate=) rather than a chmod. If you hand-edit the tree while systemd manages it, systemd may overwrite you, so for anything long-lived use unit files or drop-ins instead.

Make it permanent, and watch for pressure

For a service that should always be bounded, put the same properties in a drop-in instead of typing flags. Run systemctl edit myapp.service and add the lines below. Changes apply on the next start, or you can apply the new limits to a running unit with systemctl set-property.

ini
[Service]
CPUQuota=150%
MemoryHigh=1500M
MemoryMax=2G
TasksMax=512
/etc/systemd/system/myapp.service.d/override.conf

Every cgroup also has a memory.pressure file in the pressure stall information format, so you can see how much a limited workload is stalling. If you read our PSI guide, this is the same data per group instead of system wide, and it tells you whether your memory.high is too tight.

To audit what a running unit is really allowed to do, ask systemd rather than guessing. The command systemctl show hog.scope -p CPUQuotaPerSecUSec -p MemoryHigh -p MemoryMax -p TasksMax prints the effective values, and systemd-cgtop gives a live top-style view of CPU, memory and I/O per cgroup. Between those two tools you can answer which service is the noisy neighbour in under a minute, which is exactly the question you will be asked when a shared server starts to crawl. Pair the numbers with the memory.events counters, because a high count that keeps rising means the limit is doing its job, while a rising oom_kill count means the limit is too low for the workload.

Gotchas

  • On a user session, systemd-run --user only gets controllers that were delegated to the user manager. If the limit seems ignored, check cat /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.controllers and fall back to sudo.
  • Limits apply to the group as a whole, children included. A cap of 1G is shared by every process the job spawns.
  • Containers add their own layer. Docker and Kubernetes translate their own memory and CPU flags into these same files.
  • A tight memory.high on an I/O heavy job can look like a hang, since the process spends its life in reclaim. Raise it or watch memory.pressure.

Takeaway

cgroup v2 is the kernel feature behind containers, Kubernetes limits and systemd services, and systemd-run lets you borrow it for any command. Start every risky build or import with a CPUQuota, a MemoryHigh and a MemoryMax, and your desktop will keep responding even when the job is having a bad day.

SOURCES

  1. Control Group v2 — The Linux Kernel documentation
  2. systemd.resource-control(5) — Debian manpages
  3. Using cgroups-v2 to control distribution of CPU time for applications — Red Hat

Frequently asked questions

What is the difference between memory.high and memory.max?

memory.high is a soft limit: going over it throttles the group and forces reclaim but does not kill. memory.max is a hard limit: exceeding it invokes the OOM killer inside that cgroup.

How do I limit CPU usage of a command on Linux?

Run it with systemd-run --scope -p CPUQuota=50% command. systemd translates the quota into cpu.max for a transient cgroup.

How do I know whether my system uses cgroup v2?

Run stat -fc %T /sys/fs/cgroup. The output cgroup2fs means the unified v2 hierarchy is mounted.

Can a limit stop a fork bomb?

Yes. Setting TasksMax in systemd, which maps to pids.max, caps the number of tasks in the group, so a runaway fork loop hits the ceiling and fails instead of freezing the machine.

NEWSLETTER

Get the engineering notes in your inbox.

Kernels, GPUs, networks, AI infrastructure — and the things we broke along the way. No spam, one click to leave.

We store only your email address, to send you new articles. See our privacy policy.

TAGS

Linuxcgroup v2systemdsystemd-runmemory.highcpu.maxresource limits

RELATED

ENGINEERING NOTES

Things we built.Things we broke.Things we learned.