Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Use cgroups (control groups) to place processes in a hierarchy and enforce CPU, memory, I/O, and process-count policies. On a systemd host, the safest method is usually a transient scope or service-unit settings such as CPUQuota= and MemoryMax=. When systemd is not managing the workload, create a cgroup v2 directory, enable controllers, write the process ID to cgroup.procs, and configure files including cpu.max, memory.max, io.max, and pids.max.

What a Linux cgroup does

A control group, or cgroup, is a hierarchical group of processes with policies for distributing and limiting system resources. Every process belongs to a cgroup hierarchy. A parent cgroup can constrain all of its descendants, so a child cannot use more than the effective limits imposed by its ancestors.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Linux kernel documentation describes a cgroup as “a mechanism to organize processes hierarchically and distribute system resources along the hierarchy in a controlled and configurable manner.” In cgroup v2, a single unified hierarchy exposes controller files for CPU, memory, I/O, process counts, and other resources. The kernel man-pages project identifies cgroup v2 as official beginning with Linux 4.5 (2025 edition).

Cgroups are not a process scheduler, virtual machine, or security boundary by themselves. They control accounting and resource distribution. Permissions, namespaces, service supervision, and container isolation are separate concerns.

Choose the management layer first

Approach Best scope Typical controls Important trade-off
Transient systemd scope One command or short-lived workload CPUQuota=, MemoryMax=, TasksMax= Easy to start and clean up; the scope ends when its processes exit.
systemd service unit Long-running daemon CPUQuota=, CPUWeight=, MemoryMax=, I/O limits, TasksMax= Persistent configuration and systemd ownership; changes require a daemon reload.
Direct cgroup v2 files Hosts without systemd, custom supervisors, or low-level tooling cpu.max, memory.max, memory.high, io.max, pids.max Maximum control, but you must handle hierarchy, delegation, permissions, cleanup, and race conditions.
Container or orchestrator settings Containers and pods Runtime or pod CPU and memory requests/limits translated to cgroup settings The runtime and kubelet are the writers; host cgroup mode and driver affect observed behavior.

Do not have multiple managers write the same cgroup. The systemd cgroup v2 interface requires each individual cgroup to have a single writer. If systemd owns a service cgroup, configure the unit rather than editing that directory behind systemd’s back.

Check whether the host uses cgroup v2

Run these commands before setting a limit:

stat -fc %T /sys/fs/cgroup
cat /sys/fs/cgroup/cgroup.controllers
systemctl --version

A cgroup v2 host reports cgroup2fs from the stat command. The cgroup.controllers file lists controllers available at the root of the mounted hierarchy, such as cpu, memory, io, and pids. Availability depends on kernel configuration, boot parameters, and delegation. A controller must be enabled in a parent before its interface files appear in a child.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To see where a shell or service currently belongs, inspect its status and cgroup path:

cat /proc/$$/cgroup
systemctl status example.service
systemd-cgls

On a systemd host, systemd-cgls displays the service and slice hierarchy. systemd-cgtop provides a live view of cgroup CPU, memory, and task activity when the relevant accounting is available.

Limit a one-off command with a transient systemd scope

A scope lets systemd manage a command without requiring a permanent unit file. This is generally the least error-prone method for an interactive or batch job on a systemd host.

  1. Start the command with the desired properties:
systemd-run --scope -p CPUQuota=50% -p MemoryMax=1G -- ./workload
  • CPUQuota=50% requests no more than half of one CPU over time. A value of 200% permits up to two CPUs’ worth of aggregate CPU time, subject to available CPUs and ancestor limits.
  • MemoryMax=1G maps to a hard cgroup memory limit. If memory cannot be reclaimed, the cgroup OOM path can terminate a process in the group.
  1. In another terminal, identify the transient unit and inspect it:
systemctl list-units --type=scope
systemctl status workload.scope
systemd-cgtop
  1. After the command exits, confirm that the scope is gone or no longer contains tasks. If the workload forks children, they remain controlled because systemd tracks the entire scope cgroup.

A scope is appropriate when the limit should follow one invocation. For a daemon that must be restarted or enabled at boot, use a service unit instead.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Apply persistent limits to a systemd service

Create a drop-in rather than editing a vendor unit directly. For a unit named example.service:

sudo systemctl edit example.service

Add:

[Service]
CPUQuota=50%
CPUWeight=200
MemoryMax=1G
IOReadBandwidthMax=/dev/nvme0n1 20M
TasksMax=256

Save the drop-in, then load and restart the service:

sudo systemctl daemon-reload
sudo systemctl restart example.service
systemctl status example.service

The properties have different purposes:

Property Resource model What it means
CPUQuota=50% Hard bandwidth cap Caps aggregate CPU time over scheduling periods at half of one CPU. It does not pin a process to a core, set CPU frequency, or guarantee latency.
CPUWeight=200 Proportional weight Sets relative CPU priority when competing cgroups are runnable. It is not an absolute ceiling; a group can use more CPU when spare capacity exists.
MemoryMax=1G Hard memory ceiling Maps to the cgroup’s maximum memory boundary. Reclaim and the cgroup OOM mechanism apply when usage cannot be reduced.
IOReadBandwidthMax=/dev/nvme0n1 20M I/O bandwidth cap Limits reads from the specified block device to the stated rate. Use the device node that actually serves the workload.
TasksMax=256 Process-count ceiling Limits the number of tasks in the service cgroup, including threads where applicable. A process that cannot create another task will receive a resource error.

Choose either a hard cap or proportional priority according to the goal. A CPU-intensive batch job that must never consume more than a fraction of a machine needs CPUQuota=. A group that should receive a larger share only during contention is better served by CPUWeight=. The two settings can be combined when you need both a priority relationship and an upper bound.

Inspect the effective settings and recent failures:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
systemctl show example.service -p CPUQuotaPerSecUSec -p CPUWeight -p MemoryMax -p TasksMax
journalctl -u example.service --since today
systemd-cgtop

If a drop-in appears correct but behavior has not changed, check that the service was restarted after daemon-reload, that the process is in the expected unit, and that an ancestor slice is not imposing a lower limit.

Configure cgroup v2 directly

Direct file operations are useful when systemd is not the manager or when a purpose-built supervisor owns a delegated subtree. The example below assumes that /sys/fs/cgroup is a writable cgroup v2 mount and that you have the required privileges.

1. Create a child cgroup

sudo mkdir /sys/fs/cgroup/my-workload

2. Enable controllers in the parent

Controllers are enabled for descendants through the parent’s cgroup.subtree_control. Enable only controllers exposed by cgroup.controllers:

cat /sys/fs/cgroup/cgroup.controllers
sudo sh -c 'printf "+cpu +memory +io +pids" > /sys/fs/cgroup/cgroup.subtree_control'

If a controller is unavailable, delegated, or blocked by the hierarchy, the write fails. Do not assume every kernel exposes every controller.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

3. Set the child’s policies

sudo sh -c 'printf "50000 100000" > /sys/fs/cgroup/my-workload/cpu.max'
sudo sh -c 'printf "1G" > /sys/fs/cgroup/my-workload/memory.max'
sudo sh -c 'printf "256" > /sys/fs/cgroup/my-workload/pids.max'

cpu.max contains a quota and period in microseconds. The example allows 50,000 microseconds of CPU time in each 100,000-microsecond period, equivalent to 50% of one CPU. A value of max in the quota position removes the quota, subject to parent constraints.

4. Place the process in the cgroup

sudo sh -c 'printf "%s" 12345 > /sys/fs/cgroup/my-workload/cgroup.procs'

Replace 12345 with the workload PID. Writing to cgroup.procs moves the process and its subsequently created descendants according to cgroup rules. Put the process in the target group before it allocates significant resources when possible; a process can otherwise consume resources in its original group first.

5. Apply an I/O rule when needed

cat /sys/fs/cgroup/my-workload/io.controllers
sudo sh -c 'printf "8:0 rbps=20971520" > /sys/fs/cgroup/my-workload/io.max'

The device identifier must match the block device’s major and minor numbers. Obtain them with lsblk or stat; do not copy 8:0 blindly to a different machine. The exact io.max syntax supports per-device limits such as read or write bytes per second, IOPS, and latency controls depending on the kernel interface.

6. Verify that the process is controlled

cat /sys/fs/cgroup/my-workload/cgroup.procs
cat /sys/fs/cgroup/my-workload/cpu.max
cat /sys/fs/cgroup/my-workload/cpu.stat
cat /sys/fs/cgroup/my-workload/memory.current
cat /sys/fs/cgroup/my-workload/memory.events
cat /sys/fs/cgroup/my-workload/io.stat
cat /sys/fs/cgroup/my-workload/pids.current

Remove the directory only after it contains no processes:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
sudo rmdir /sys/fs/cgroup/my-workload

Manual changes under a system-managed hierarchy are not persistent across reboot and may be overwritten. For repeatable service policy, encode the settings in the manager that owns the cgroup.

Understand CPU controls

CPU quota is a time-bandwidth limit. It regulates aggregate CPU time over scheduler periods; it does not lock a process to a particular CPU, choose a frequency, or promise a response-time bound. A quota can therefore protect overall capacity while still allowing short bursts within a period.

Weights are different. A weight expresses a relative share among competing cgroups. If a sibling is idle, a weighted group may use most of the machine. If several siblings are busy, their effective shares depend on weights and all ancestor limits. Use quota for “never exceed this amount” and weight for “prefer this group when there is contention.”

Inspect cpu.stat for usage and throttling counters. A rising throttled-time counter indicates that the quota, rather than lack of runnable work, is limiting execution. If a service is slow despite a generous local quota, inspect parent slices and system-wide CPU pressure.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Understand memory.max and memory.high

File or property Behavior When to use it
memory.high Reclaim and throttling boundary. The kernel applies pressure to the cgroup and slows it while reclaiming; it does not directly invoke the cgroup OOM killer. Shape memory growth and create an early-warning boundary before a hard failure.
memory.max / MemoryMax= Hard maximum. When usage cannot be reduced, the cgroup OOM path may kill a process in the group. Enforce a firm ceiling for a service or workload that can tolerate an OOM failure mode.
memory.current Current accounted usage for the cgroup. Observe consumption and compare it with configured boundaries.
memory.events Event counters for high, max, OOM, and related conditions. Detect reclaim pressure, limit hits, and OOM activity.

Memory accounting includes major user-space allocations, page cache, and several kernel structures. Coverage can expand as kernel accounting gains support for additional memory types. A cgroup’s configured maximum is not an unconditional byte-for-byte instantaneous cutoff: it can be exceeded temporarily in some circumstances, and not every allocation path invokes the OOM killer. Treat it as a control boundary with an observable failure mode.

Set memory.high below memory.max when you want reclaim pressure before a hard stop. Monitor memory.events and application latency; a high boundary that is too low can cause sustained reclaim and make a service appear hung even though it has not reached the maximum.

Use hierarchy deliberately

Limits are cumulative. If a service has MemoryMax=1G but its parent slice allows only 512 MiB, the effective ceiling is constrained by the parent. The same rule applies to CPU quota, I/O controls, and task counts. A child cannot escape an ancestor’s limit by writing a larger value locally.

Conversely, a parent policy can unintentionally throttle unrelated services if they share the same slice. Inspect the hierarchy with systemd-cgls or by walking cgroup.procs and cgroup.subtree_control before moving processes. Design slices so that workloads with different service-level objectives do not inherit a constraint meant for another group.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Observe limits, pressure, and failures

Configuration alone does not prove that a limit is active. Pair each policy with an observable signal:

  • CPU: cpu.stat for usage and throttling; systemd-cgtop for a live view.
  • Memory: memory.current against memory.max and memory.high; memory.events for reclaim, max, and OOM counters.
  • I/O: io.stat for per-device activity and throttling information.
  • Tasks: pids.current against pids.max; service logs for failed thread or process creation.
  • Service state: systemctl status, systemctl show, and the journal for restart, timeout, or OOM symptoms.

Linux pressure-stall information can explain why a limit causes user-visible delay even before a hard ceiling is reached. Compare pressure and cgroup event counters with application metrics rather than judging success only by a single instantaneous reading.

Best Value
Sale
UNIX and Linux System Administration Handbook, 4th Edition
  • New
  • Mint Condition
  • Dispatch same day for order received before 12 noon
  • Guaranteed packaging
  • No quibbles returns

Common failure modes and fixes

The controller file is missing

The controller may not be enabled in the parent, may not be compiled into the kernel, or may not be delegated to your user. Check cgroup.controllers at each level and enable the controller in the parent’s cgroup.subtree_control where you have permission.

The write is rejected with a busy or invalid-operation error

Some cgroup operations require the parent’s process-placement rules to be satisfied. In particular, a domain cgroup generally must have no member processes before enabling controllers for its descendants. Move processes to an appropriate child or manager-owned unit, then retry.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The limit appears ignored

Confirm cgroup v2 is mounted, verify the process PID in the target cgroup.procs, inspect ancestor limits, and check that another manager has not moved the process. For systemd, use systemctl show to inspect the effective unit properties and restart after reloading the configuration.

A service is killed near its memory limit

Read memory.events and service logs. A rising oom or oom_kill counter indicates the hard path, while rising high events indicate reclaim and throttling. Raise memory.max, reduce the workload’s footprint, or introduce a higher memory.high threshold if the failure is premature for the application.

CPU latency worsens after setting a quota

Quota controls time over a period, not latency. Check throttling in cpu.stat. If the goal is fair sharing rather than an absolute cap, replace or supplement the quota with CPUWeight=; if the workload is latency-sensitive, size the quota and period with the application’s burst pattern in mind.

Manual settings disappear

Files under /sys/fs/cgroup represent live kernel state. Reboots, service restarts, and systemd reconciliation can remove manual settings. Put persistent policy in a service drop-in or the configuration system that owns the delegated subtree.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Containers and Kubernetes

Container runtimes and Kubernetes use cgroups to enforce pod and container CPU and memory limits. The runtime or kubelet translates resource settings into cgroup configuration, so a container’s behavior depends on the host’s cgroup mode, kernel controllers, and runtime driver.

When a pod does not behave as expected, inspect the node’s cgroup filesystem and the runtime’s driver before changing application settings. A pod limit can be correct while an ancestor slice, node-level reservation, or unavailable controller changes the effective result. Avoid manually editing a container’s manager-owned cgroup; use the container runtime or Kubernetes specification so that reconciliation does not undo the change.

A practical decision checklist

  • Use a transient systemd scope for one command on a systemd host.
  • Use a service-unit drop-in for a daemon that must retain its limits across restarts.
  • Use direct cgroup v2 files only when you own a delegated subtree or systemd is not the manager.
  • Use CPUQuota= or cpu.max for an absolute CPU ceiling; use CPUWeight= for relative priority.
  • Use memory.high for throttling and reclaim pressure; use memory.max for a hard boundary with a possible OOM failure.
  • Enable controllers before using their files and verify that ancestors do not impose tighter limits.
  • Record baseline usage, then verify with controller statistics and event counters.
  • Leave ownership to one manager per cgroup.

Minimal worked example

For a service that should use at most half a CPU, no more than 1 GiB of accounted memory, and 256 tasks, create a drop-in containing:

[Service]
CPUQuota=50%
MemoryMax=1G
TasksMax=256

Apply it with:

sudo systemctl daemon-reload
sudo systemctl restart example.service
systemctl show example.service -p CPUQuotaPerSecUSec -p MemoryMax -p TasksMax
systemctl status example.service

Then monitor systemd-cgtop, the service journal, and the cgroup’s CPU, memory, and task counters. This confirms both the configured policy and its runtime effect without bypassing systemd’s ownership of the service cgroup.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.