Skip to content

CPU, Memory, and IO Limits for systemd Services

With systemd, you can assign services to cgroups and control resources such as CPU time, memory, task count, and block IO on a per-service basis.

This article targets RHEL 8-compatible Linux systems. We will configure the following limits for a test service and verify not only the configured values but also that the limits actually work by generating real workloads.

  • CPUQuota=20%: Limits CPU time to a maximum equivalent to 20% of one CPU
  • MemoryMax=64M: Sets an absolute upper limit on the memory that the service’s cgroup can use
  • TasksMax=20: Limits the number of tasks that can exist simultaneously within the service

CPUQuota= functions as an absolute limit on CPU time. MemoryMax= is a hard limit on memory usage, and if sufficient memory cannot be reclaimed within the limit, OOM handling may occur within the cgroup. TasksMax= limits the number of tasks, including not only processes but also threads.

For IO, the setting names differ depending on the cgroup version. With cgroup v1, commonly used in RHEL 8, you can use BlockIOWeight= and per-device settings such as BlockIOReadBandwidth= and BlockIOWriteBandwidth=. With cgroup v2, settings such as IOWeight=, IOReadBandwidthMax=, and IOWriteBandwidthMax= are used. Because bandwidth limits depend on the target block device, the general-purpose load tests in this article measure CPU, memory, and task count.

First, create a script and a systemd service that can switch between CPU, memory, and task-count workloads. Under normal conditions, the service waits with a low load and changes the workload type according to the contents of /run/resource-demo.mode.

sudo tee /usr/local/sbin/resource-demo.sh >/dev/null <<'EOF'
#!/bin/bash
set -u
mode="$(cat /run/resource-demo.mode 2>/dev/null || printf 'idle')"
case "$mode" in
cpu)
exec sha256sum /dev/zero
;;
memory)
exec dd if=/dev/zero of=/dev/null bs=128M count=1
;;
tasks)
while sleep 60 & do
:
done
echo TASK_LIMIT_HIT
wait
;;
*)
exec sleep 300
;;
esac
EOF
sudo chmod 755 /usr/local/sbin/resource-demo.sh
sudo tee /etc/systemd/system/resource-demo.service >/dev/null <<'EOF'
[Unit]
Description=Resource control example service
[Service]
Type=simple
ExecStart=/usr/local/sbin/resource-demo.sh
Restart=no
EOF
sudo systemctl daemon-reload

Step 2: Set CPU, memory, and task-count limits

Section titled “Step 2: Set CPU, memory, and task-count limits”

You can use systemctl set-property to configure resource-control properties for a service. Here, we limit CPU usage to 20%, memory to 64 MiB, and the task count to 20.

Terminal window
sudo systemctl set-property resource-demo.service CPUQuota=20% MemoryMax=64M TasksMax=20
sudo rm -f /run/resource-demo.mode
sudo systemctl restart resource-demo.service
sudo systemctl is-active resource-demo.service

Values configured with set-property are applied as service settings managed by systemd. You do not need to edit the existing Unit file directly.

After configuring the limits, use systemctl show to verify the effective values recognized by systemd. systemctl show is also suitable for displaying properties in a machine-readable format.

Terminal window
sudo systemctl show resource-demo.service \
-p CPUQuotaPerSecUSec \
-p MemoryMax \
-p TasksMax

In this example, you can verify that the internal representation of CPUQuota is 200ms, 64 MiB is 67108864 bytes, and TasksMax is 20.

Switch to CPU mode and restart the service. Because sha256sum /dev/zero continuously uses CPU resources, it can be used to verify CPUQuota.

Terminal window
sudo tee /run/resource-demo.mode >/dev/null <<'EOF'
cpu
EOF
sudo systemctl restart resource-demo.service

Next, measure the increase in CPU usage time over five seconds. With CPUQuota=20%, using one CPU as the reference, approximately one second of CPU time can be used during a five-second interval. To allow for scheduling and measurement boundaries, we verify here that the value remains below two seconds.

Terminal window
sudo bash -c '
before=$(systemctl show resource-demo.service -p CPUUsageNSec --value)
sleep 5
after=$(systemctl show resource-demo.service -p CPUUsageNSec --value)
delta=$((after - before))
printf "CPU usage delta: %s ns\n" "$delta"
test "$delta" -gt 0
test "$delta" -lt 2000000000
printf "CPU_QUOTA_OK\n"
'

This confirms that CPU time does not increase without limit under load and is instead throttled according to the configured quota.

Next, switch to memory mode. The test service starts dd, which requires a 128 MiB buffer, while the service has MemoryMax=64M configured.

Terminal window
sudo tee /run/resource-demo.mode >/dev/null <<'EOF'
memory
EOF
sudo systemctl restart resource-demo.service || true

The amount of memory requested by the service exceeds the configured limit. Check the service’s termination state and the journal to confirm that the process could not continue because of the memory limit.

Terminal window
sudo bash -c '
state=$(systemctl show resource-demo.service -p ActiveState --value)
journalctl -u resource-demo.service --since "-2 minutes" --no-pager | tail -n 30
test "$state" != "active"
printf "MEMORY_LIMIT_OK\n"
'

MemoryMax= is not merely a warning threshold; it is an absolute upper limit on the cgroup’s memory usage. If an allocation exceeding the limit is required and sufficient memory cannot be reclaimed, processes within the service may be subject to OOM handling.

Step 6: Verify task-count limiting with TasksMax

Section titled “Step 6: Verify task-count limiting with TasksMax”

Next, switch to task-generation mode. The test script repeatedly creates background sleep processes.

Terminal window
sudo tee /run/resource-demo.mode >/dev/null <<'EOF'
tasks
EOF
sudo systemctl restart resource-demo.service

Once TasksMax=20 is reached, attempts to create additional tasks are rejected. Check the current task count and the journal records.

Terminal window
sudo bash -c '
tasks=$(systemctl show resource-demo.service -p TasksCurrent --value)
printf "TasksCurrent=%s\n" "$tasks"
test "$tasks" -le 20
journalctl -u resource-demo.service --since "-2 minutes" --no-pager | tail -n 30
printf "TASK_LIMIT_OK\n"
'

TasksMax= does not count only processes. In Linux cgroups, threads are also treated as tasks, so multithreaded services may reach the limit sooner than the process count alone would suggest.

After verification, remove the mode file and restart the service. This stops workload generation and returns the service to its normal waiting state.

Terminal window
sudo rm -f /run/resource-demo.mode
sudo systemctl restart resource-demo.service

Finally, verify that the service is running normally under systemd management.

Terminal window
sudo systemctl show resource-demo.service -p ActiveState -p SubState

If ActiveState=active and SubState=running, the service has successfully returned to its normal state after the load tests.

Considerations when configuring IO control

Section titled “Considerations when configuring IO control”

Unlike CPU and memory control, IO control requires checking both the cgroup version in use and the target block device.

With cgroup v1, in addition to relative bandwidth allocation using BlockIOWeight=, you can set per-device bandwidth limits with BlockIOReadBandwidth= and BlockIOWriteBandwidth=. With cgroup v2, properties such as IOWeight=, IOReadBandwidthMax=, and IOWriteBandwidthMax= are used.

A weight represents relative allocation during contention and differs from an absolute bandwidth limit. If the objective is to prevent read or write throughput from exceeding a specific rate, use a per-device bandwidth limit rather than a weight.

In RHEL 8, some environments use cgroup v1 as the standard configuration, while cgroup v2 is also supported. When designing IO control for a production environment, check the cgroup configuration with a command such as stat -fc %T /sys/fs/cgroup, and select the systemd properties appropriate for the cgroup version in use.

systemd resource control allows you to manage cgroup constraints on a per-Unit basis.

CPUQuota= can be used as a limit on CPU time, MemoryMax= as an absolute upper limit on memory usage, and TasksMax= as a limit on the number of tasks. After configuration, use systemctl show to verify the effective values. By then generating actual workloads, you can also confirm that the settings do not merely exist in the configuration but function as kernel-level controls.

When applying these limits to production services, first observe current peak resource usage and start with values that provide sufficient headroom. In particular, setting MemoryMax= or TasksMax= too low can directly cause service termination or failures when starting new work.

Category: Linux