Moving Beyond the Individual
ulimit works at the level of a single process or user session. But real services are not single processes — Nginx spawns dozens of worker processes, a Java application server forks threads, a batch job launches hundreds of parallel tasks. You need a way to apply a resource budget to an entire group of processes as a single unit.
Control Groups (cgroups) solve this. A cgroup is a kernel-managed collection of processes. You place processes into a cgroup, then attach resource controllers to that cgroup. The controllers enforce limits on the group as a whole, regardless of how many individual processes are inside it.
Think of it like a departmental expense budget. The marketing department gets $50,000 per quarter. It doesn't matter if there are 5 people or 50 people in the department — the total spend cannot exceed $50,000. The finance team (the kernel) tracks all spending and stops any transaction that would exceed the budget.
The key resource controllers:
| Controller | What It Controls | Example Use |
|---|---|---|
memory |
Total RAM + swap usage | Limit a container to 512 MB |
cpu |
CPU time share (weight or quota) | Guarantee a service gets 25% of a core |
cpuset |
Which CPU cores a group can use | Pin a latency-sensitive service to cores 0-1 |
blkio / io |
Disk read/write bandwidth and IOPS | Prevent a backup job from saturating disk I/O |
pids |
Maximum number of processes in the group | Prevent fork bombs at the group level |
net_cls |
Tag network packets for traffic shaping | Apply QoS rules to a specific service's traffic |
How the kernel enforces memory limits: When a cgroup's memory usage reaches its limit, the kernel first tries to reclaim memory by evicting page cache (cached file data). If that is insufficient, it invokes the OOM killer — but crucially, the OOM killer is scoped to the cgroup. It kills processes inside the cgroup, not random processes on the host. This is what makes containers safe: a memory-leaking container kills its own processes, not the host's.
Interacting with cgroups directly (cgroups v2, the modern interface): the cgroup hierarchy lives under /sys/fs/cgroup/ as plain directories, and each controller's settings — like a service's memory.max — are readable as ordinary text files. A process's own cgroup membership is likewise visible via /proc/<pid>/cgroup.
In practice, you rarely interact with cgroup files directly. systemd manages cgroups for system services, and container runtimes (Docker, containerd) manage them for containers. But understanding the underlying mechanism explains why systemctl set-property nginx.service MemoryMax=512M works — it is writing to the cgroup filesystem on your behalf.