CPU and Memory Limits Without a Container
If you only need to limit a trusted worker’s resources, you do not need to package it in a Docker image first. cgroups v2 groups processes, accounts for resource use and applies limits. On a systemd host, systemd-run is often enough.
This is resource control, not a complete sandbox. Cgroups alone do not separate filesystems or networks and do not replace permission controls. Containers combine several mechanisms; their configuration does not always require OverlayFS or a separate network.
cgroups v2: One Hierarchy Instead of a Tree per Controller
With v1, controllers could use separate hierarchies. In v2 they operate in a shared tree. In the ordinary domain model, a process belongs to one cgroup in that hierarchy and children inherit membership. Threaded mode has additional rules that this example does not use.
Check the Mount and Available Controllers
mount | grep cgroup
cat /sys/fs/cgroup/cgroup.controllerscgroup2 confirms a v2 mount. Its presence alone does not prove that v1 is absent. Available controllers depend on kernel configuration; in a subgroup, they also depend on what the parent makes available.
Direct cgroupfs Management
Creating a directory creates a cgroup. Writing a PID to cgroup.procs moves a process, and controller files accept limits. You must follow hierarchy rules and have permission to manage that subtree. On a systemd-managed host, do not create arbitrary groups alongside its units.
Enable Controllers in the Parent
The following assumes an existing, empty parent cgroup delegated to you, with cpu and memory available. Supply its actual path. This demonstrates configuration, not the complete worker lifecycle.
# CGROUP_PARENT must point to your delegated subtree.
: "${CGROUP_PARENT:?Set the path of an empty delegated cgroup}"
printf '+cpu +memory\n' > "$CGROUP_PARENT/cgroup.subtree_control"
mkdir "$CGROUP_PARENT/devmindset-worker"
printf '20000 100000\n' > "$CGROUP_PARENT/devmindset-worker/cpu.max"
printf '268435456\n' > "$CGROUP_PARENT/devmindset-worker/memory.max"cgroup.subtree_control makes controllers available to children. Do not enable domain controllers in the worker group where you intend to place processes: the no-internal-processes rule applies, with exceptions described in the kernel documentation.
cpu.max accepts quota and period in microseconds. 20000 100000 gives the group a combined budget equivalent to 20% of one CPU, not a separate allowance for each process. memory.max caps memory charged by the controller; unsuccessful reclaim can lead to an OOM event within the group.
Starting a process and moving its PID afterward leaves a window in which it runs outside the limit and can spawn descendants. A service manager is usually more convenient for launching jobs.
systemd-run: A Transient Unit with Limits
This uses the user manager and requires a working systemd session with the relevant controllers available. Replace the worker path with an existing file. Using the system manager requires separate decisions about permissions and the service user.
systemd-run --user --wait --pipe --collect \
--unit=devmindset-worker \
--property=Type=exec \
--property=CPUQuota=20% \
--property=MemoryMax=256M \
/usr/bin/python3 /opt/workers/pipeline_runner.py
# In a second terminal, while the unit is running:
systemctl --user status devmindset-worker.service
systemctl --user show devmindset-worker.service -p ControlGroupA transient service is the default; there is no need for a --service option. --scope selects a different model, where systemd-run starts the command and it inherits the caller’s environment. --wait waits for service completion, --pipe passes standard streams, and --collect allows the unit to be collected afterward.
Add IOWeight when you need a relative share of I/O. It is not a bytes-per-second limit; its effect depends on the controller and device stack. Do not add it merely because a sample command contained it.
Launching from Python
You do not need your own cgroupfs controller to launch resource-limited jobs. Python can invoke the same manager. An argument list avoids constructing a shell command, and unique names distinguish concurrent units.
import subprocess
from pathlib import Path
from uuid import uuid4
def run_worker(script: Path) -> None:
script = script.resolve(strict=True)
subprocess.run([
"systemd-run", "--user", "--wait", "--pipe", "--collect",
"--expand-environment=no",
f"--unit=devmindset-worker-{uuid4().hex}",
"--property=Type=exec",
"--property=CPUQuota=20%",
"--property=MemoryMax=256M",
"/usr/bin/python3", str(script),
], check=True)
if __name__ == "__main__":
run_worker(Path("/opt/workers/cpu_intensive_task.py"))--expand-environment=no requires systemd 254 or later and disables systemd’s expansion of variables in arguments. The code assumes a trusted script path and an interpreter at the stated location. Manager or job failures reach the caller through CalledProcessError. This does not isolate untrusted code.
Resource Limits or a Container Environment?
| Need | Mechanism to consider |
|---|---|
| CPU and memory limits for a trusted worker | cgroups v2, for example through a systemd unit |
| Packaging an application and dependencies in an image | Container tools |
| A separate view of files, processes or networking | Namespaces and suitable permissions; cgroups are only part of the solution |
| Running an untrusted program | A separate isolation design and threat model |
There is no need for a universal table of milliseconds and megabytes. Compare solutions under the same requirements. Direct resource limits can simplify worker execution, but do not provide every property of a container.
Choose the Mechanism for the Problem
If the problem is “this process uses too much CPU,” start with a limit and measurement. If it is “I must deliver an application with dependencies and separate its environment,” a cgroup alone is insufficient. Understanding kernel mechanisms helps identify what the particular case requires.
Child membership is easier to understand with fork(). For growing memory usage, also check the difference between freeing allocations and reducing RSS.
Technical references: cgroups v2 hierarchy and controllers, systemd-run, systemd resource limits.


