cgroups are the unsung hero of resource management on Linux, and they don’t actually limit resources per process in the way you might expect.

Imagine you have a busy server. You want to make sure one runaway application doesn’t hog all the CPU, starving the other critical services. Or maybe you have a batch job that you want to contain so it doesn’t consume all available RAM and crash the system. This is where cgroups come in. They’re a Linux kernel feature that allows you to allocate, limit, and prioritize system resources—like CPU time, memory, I/O, and network bandwidth—for groups of processes.

Let’s see this in action. We’ll create a cgroup, set some limits, and then run a process within it.

First, we need to mount the cgroup filesystem. This is usually done under /sys/fs/cgroup.

sudo mkdir -p /sys/fs/cgroup/my_limited_group

Now, let’s set a CPU limit. We’ll use the cpu.shares parameter. This value is relative; a process in a cgroup with cpu.shares=1024 gets the same proportion of CPU as a process in another cgroup with cpu.shares=1024 if they are the only two. If we have one with 512 and another with 1024, the second one gets twice as much CPU. For a hard limit, we often use cpu.cfs_quota_us and cpu.cfs_period_us. cpu.cfs_period_us defines the time slice (in microseconds), and cpu.cfs_quota_us defines how much CPU time the cgroup can use within that period. A common setting for 50% CPU is cpu.cfs_quota_us=50000 and cpu.cfs_period_us=100000.

# Set CPU quota to 50% of one core
echo 50000 | sudo tee /sys/fs/cgroup/my_limited_group/cpu.cfs_quota_us
echo 100000 | sudo tee /sys/fs/cgroup/my_limited_group/cpu.cfs_period_us

Next, memory. We’ll set a hard limit using memory.limit_in_bytes. Let’s say we want to limit it to 100MB.

# Set memory limit to 100MB
echo 100M | sudo tee /sys/fs/cgroup/my_limited_group/memory.limit_in_bytes

Now, we need to add a process to this cgroup. The easiest way is to launch a new process directly into it. Let’s use systemd-run for this, as it’s the modern way to manage services and tasks with cgroups.

# Launch a 'stress' command that consumes CPU and memory
sudo systemd-run --unit=my-stress-test --scope -p CPUShares=512 -p CPUQuota=50% -p MemoryLimit=100M stress --cpu 1 --vm 1 --vm-bytes 150M --timeout 60s

The stress command will try to use one CPU core and allocate 150MB of memory. Because of the cgroup limits, it will be throttled in its CPU usage to 50% of one core and will likely be OOM (Out Of Memory) killed by the kernel when it tries to exceed the 100MB limit.

Let’s look at the mental model. Cgroups are hierarchical. You can create sub-cgroups within existing ones. This allows for complex resource delegation. For instance, a user might have a top-level cgroup, and within that, they might have separate cgroups for their web server, database, and background worker processes, each with its own finely tuned resource allocations. The kernel then enforces these limits by intercepting system calls related to resource allocation and checking them against the configured limits for the cgroup the process belongs to.

The "per process" part is a bit of a misnomer. Cgroups manage groups of processes. When you add a process to a cgroup, all its children inherit that cgroup membership. The limits are applied to the aggregate resource usage of all processes within that cgroup, not to individual processes within the group. If you have two processes in a cgroup limited to 100MB, they share that 100MB. If one process tries to use 90MB and the other 20MB, the second one will be killed by the OOM killer, even though the first one is well within its "share."

What most people don’t realize is how deeply integrated cgroups are with systemd. While you can manually manipulate cgroup files, systemd’s unit files and systemd-run provide a much more robust and user-friendly interface. When you enable a systemd service, it’s automatically placed into a cgroup defined by its service type and name, allowing for system-wide resource control without manual intervention for most common scenarios. The system.slice and user.slice are fundamental parts of this, organizing all running processes.

The next thing you’ll run into is understanding how cgroup v1 and v2 differ, especially concerning unified hierarchies and new controllers.

Want structured learning?

Take the full Linux & Systems Programming course →