Cap a Command’s CPU and Memory Use with Ubuntu cgroups

If one command makes your Ubuntu desktop sluggish or causes a server to run short on memory, put the command and its child processes in a systemd-managed cgroup with a CPU quota and memory ceiling. On current Ubuntu releases, systemd-run --scope is the shortest practical route: it starts the command in a temporary scope, and systemd applies the limits through cgroup controls.

The example below caps a workload at half of one CPU’s capacity and 1 GiB of memory. These are limits for the whole scope, including child processes; they are not a promise that the program will run at a particular speed.

Why a process needs a cgroup limit

A busy process can compete with interactive applications, while a memory-heavy process can push the machine into reclaim or an out-of-memory condition. A cgroup groups a process with its descendants so the kernel can account for and constrain that group. Ubuntu’s systemd provides a convenient interface to those controls.

CPU and memory settings behave differently. CPUQuota=50% caps CPU time to half the capacity of one CPU, averaged over a scheduling period. A value of 200% represents up to two CPUs’ worth of time when available. This is a ceiling, not a CPU priority or a reservation. MemoryMax=1G sets a hard ceiling for memory charged to the cgroup; if the workload cannot stay below it, the kernel may reclaim memory or invoke an out-of-memory kill for a process in that cgroup. Choose a limit with headroom for the application and its children.

1. Check that Ubuntu is using cgroup v2

Ubuntu systems using systemd commonly use the unified cgroup v2 hierarchy. Confirm the active setup before relying on the paths shown later:

stat -fc %T /sys/fs/cgroup
systemctl --version | head -n 1

The first command should report cgroup2fs. If it reports another filesystem type, this article’s cgroup v2 file paths do not apply as written. The systemd properties may still be supported with a different hierarchy, but use the matching distribution documentation rather than copying v2 paths.

Also check the exact systemd version if a property is rejected. Ubuntu’s Noble documentation describes CPUQuota= and MemoryMax= as resource-control settings; older Ubuntu releases may package different systemd versions.

2. Start the workload inside a limited scope

Run the program through systemd-run, substituting its real executable and arguments. For a command in the current directory, use ./ and pass all arguments after --:

systemd-run --scope \
  -p CPUQuota=50% \
  -p MemoryMax=1G \
  -- ./my-program --input data.csv
Ubuntu terminal running systemd-run with CPUQuota set to 50 percent and MemoryMax set to 1G for a command
Launch a command inside a transient systemd scope with CPU and memory limits.

For a user-owned command, try this without sudo first. If systemd reports that a requested controller or property is unavailable, the user manager may not have permission to delegate it; retrying as root may be appropriate for a system-wide workload, but it changes the command’s security context. Do not casually run an untrusted program as root. If using the system manager as root is required, use sudo systemd-run --scope ... only after accounting for that change.

The terminal remains attached to the scope while the command runs. Pressing Ctrl+C interrupts the foreground operation. The scope is transient and normally disappears after the processes exit. It is a convenient way to start a command under limits, rather than a persistent policy for future launches.

3. Find the scope and inspect its limits

systemd-run prints the unit name it creates, typically something like run-….scope. While the command is still running, list the cgroup tree:

systemd-cgls

Find the transient scope and confirm the target program and its children appear beneath it. That matters because a child that runs outside the scope will not be covered by the same cgroup ceiling.

Terminal cgroup tree showing a transient run scope containing a program and two worker processes
Use the cgroup tree to confirm the program and its workers are grouped under the transient scope.

Use the printed scope name in the next command. Replace run-abc.scope with the actual name:

systemctl show run-abc.scope \
  -p ControlGroup -p CPUQuotaPerSecUSec -p MemoryMax

CPUQuotaPerSecUSec=50000 is consistent with a 50% quota when systemd’s quota period is 100 ms; MemoryMax=1073741824 is 1 GiB in bytes. The live values are more useful than assuming every host will accept every request unchanged, since parent cgroups and available controllers can constrain the effective result.

systemctl show output with ControlGroup, CPU quota, and memory maximum values for a transient scope
Check the scope’s reported control group and configured CPU and memory ceilings.

4. Verify kernel cgroup files and adjust safely

The ControlGroup property gives the cgroup path relative to /sys/fs/cgroup. Store it in a shell variable so the commands follow the path actually assigned on your machine:

UNIT=run-abc.scope
CG=$(systemctl show "$UNIT" -p ControlGroup --value)
printf 'Group: %s\n' "$CG"
cat "/sys/fs/cgroup$CG/cpu.max"
cat "/sys/fs/cgroup$CG/memory.max"

On cgroup v2, cpu.max contains a quota and period in microseconds. For example, 50000 100000 means 50 ms of CPU time in each 100 ms period. memory.max shows the memory ceiling in bytes. If the files are missing, confirm that the scope is still active and that the kernel is using cgroup v2.

Terminal reading cpu.max and memory.max from the systemd scope under sysfs cgroup v2
Read the kernel’s cgroup v2 files to verify the configured quota and memory ceiling.

If you started a named long-running unit and need to adjust its limits, systemd supports changing resource properties on an active unit, for example sudo systemctl set-property --runtime my-worker.service CPUQuota=50% MemoryMax=1G. This applies to that service’s cgroup and requires appropriate privileges. It does not attach an arbitrary already-running process just because its PID is known; for a one-off process, stop it cleanly and relaunch it through the limited scope. For a persistent service, configure the properties in the service unit or a systemd drop-in and restart it during an appropriate maintenance window.

Check the result

Keep the workload active while you inspect it. Confirm that systemd-cgls places the process and its children under the intended scope, that systemctl show reports the requested properties, and that the corresponding cpu.max and memory.max files exist with finite values. When the process exits, the transient scope is removed, so its cgroup files may disappear.

A quota limits CPU time but does not guarantee a fixed percentage in a desktop monitor, and a hard memory cap can cause failures if the application needs more than the cap. If the workload becomes unstable, stop it and raise the limit to a measured safe value or use a softer MemoryHigh= pressure threshold alongside a higher MemoryMax=. For exact behavior, check your installed systemd and kernel documentation; the authoritative references are the Ubuntu systemd-run manual, Ubuntu systemd resource-control manual, and Linux kernel cgroup v2 documentation.

Leave a Comment

How to Convert Three-Phase Voltage, Current, kW, kVA, and Power Factor

How to Convert Three-Phase Voltage, Current, kW, kVA, and Power Factor

Learn the balanced three-phase formulas and convert line voltage, current, kW, kVA, and power factor step by step, with a worked example and limits to check.

Cap a Command’s CPU and Memory Use with Ubuntu cgroups

Cap a Command’s CPU and Memory Use with Ubuntu cgroups

Run a workload inside a systemd scope with CPUQuota and MemoryMax, then verify the limits through cgroup v2.

Encrypt Pi-hole Upstream DNS on Ubuntu Server 24.04

Encrypt Pi-hole Upstream DNS on Ubuntu Server 24.04

Set up dnscrypt-proxy as Pi-hole’s local DoH bridge on Ubuntu 24.04, with a localhost-only listener, resolver selection, and checks before switching DNS.

Get Ubuntu 24.04 Touchpad Gestures Working on Wayland

Get Ubuntu 24.04 Touchpad Gestures Working on Wayland

Check Ubuntu’s touchpad settings, confirm Wayland, and test GNOME’s built-in swipes before troubleshooting apps or hardware.

Build a Debian 12 RAID 1 Mirror with mdadm

Build a Debian 12 RAID 1 Mirror with mdadm

Create and verify a two-disk Debian 12 RAID 1 data array, format it safely, and configure it to assemble and mount after reboot.

Encrypt Debian 12 Swap with a Fresh Key at Every Boot

Encrypt Debian 12 Swap with a Fresh Key at Every Boot

Set up random-key encryption for an existing Debian 12 swap partition, verify the active dm-crypt mapping, and understand the hibernation trade-off.

How to Restrict SSH Users to Specific Directories on Ubuntu 24.04 (SFTP Chroot Guide)

How to Restrict SSH Users to Specific Directories on Ubuntu 24.04 (SFTP Chroot Guide)

Restrict Ubuntu 24.04 SSH accounts to designated SFTP folders with chroot, proper permissions, Match rules, and safe tests—without granting shell access.

Release Ubuntu’s APT Lock Safely and Finish the Update

Release Ubuntu’s APT Lock Safely and Finish the Update

Find which process owns Ubuntu’s dpkg frontend lock, let package work finish, and repair interrupted installations without deleting lock files.

How to Set Up Automated Btrfs Snapshots on Ubuntu Desktop

How to Set Up Automated Btrfs Snapshots on Ubuntu Desktop

Set up scheduled Btrfs system snapshots on Ubuntu Desktop with Timeshift, verify the Ubuntu subvolume layout, choose retention, and understand what snapshots cannot protect.

How to Migrate Debian 12 to Testing Without Breaking Dependencies

How to Migrate Debian 12 to Testing Without Breaking Dependencies

Plan a safer Debian 12 to Testing migration: upgrade through Stable, back up, clean APT sources, simulate dependency changes, and verify what changed.