The Resource Contention Problem
A standard Ubuntu server operates under a generally democratic resource allocation model. If you run a massive data-processing script, a PostgreSQL database, and a web server on the same machine, the Linux kernel attempts to share the CPU and RAM fairly among them.
However, if the data-processing script hits an infinite loop or suddenly attempts to load a 50GB file into memory, it will consume 100% of the CPU and exhaust the system RAM. When the RAM is exhausted, the Linux Out-Of-Memory (OOM) Killer activates, and it often panics and forcefully kills the PostgreSQL database to save the system. A poorly written script just crashed your production database.
To prevent this, administrators use cgroups (Control Groups). cgroups are a fundamental Linux kernel feature (and the technology that makes Docker containers possible) that allows you to mathematically hard-limit the CPU, RAM, and disk I/O of any process. You can place the data-processing script in a cgroup that physically restricts it to 10% of one CPU core and 500MB of RAM. If the script goes rogue, it hits an invisible concrete wall; the rest of the server remains perfectly healthy.
Step 1: Understanding Systemd and Cgroups v2
In the past, managing cgroups required installing archaic utilities like libcgroup and editing complex configuration files. In modern Ubuntu releases (20.04, 22.04, and later), cgroups are managed entirely by systemd (using the Cgroups v2 architecture).
Every service you start using systemctl start is automatically placed into its own cgroup, nested under the system.slice hierarchy.
You can view the current cgroup tree of your entire operating system by running:
systemd-cgls
The output will display a hierarchical tree showing exactly which process IDs (PIDs) belong to which services.
Step 2: Applying CPU Limits (CPUQuota)
Suppose you have a custom backup script that runs as a systemd service (daily-backup.service). When it runs, the heavy compression algorithm causes the server CPU to spike to 100%, causing the web server to drop connections.
You can limit the backup service without editing the script itself. You use the systemctl set-property command to inject cgroup limits dynamically.
sudo systemctl set-property daily-backup.service CPUQuota=50%
This command instructs the kernel that the backup process (and any child processes it spawns) is never allowed to consume more than 50% of a single CPU core. If your server has 4 cores (400% total capacity), the backup script is mathematically restricted to a tiny fraction of the total processing power.
The beauty of this command is that it writes an override file directly to /etc/systemd/system.control/, meaning the limit survives a server reboot.
Step 3: Applying Memory Limits (MemoryMax)
Memory leaks are far more dangerous than CPU spikes. If you have an experimental Java application (experimental-app.service) that is known to leak memory, you must sandbox it.
sudo systemctl set-property experimental-app.service MemoryMax=2G
This sets a hard limit of 2 Gigabytes of RAM. If the Java application attempts to allocate 2.1GB, the kernel will not allow it. Instead of crashing the entire server, the kernel’s OOM killer will surgically terminate only the processes inside that specific cgroup. The application dies, but the server survives.
You can combine this with MemoryHigh, which acts as a “soft limit.”
sudo systemctl set-property experimental-app.service MemoryHigh=1.5G
If the application hits 1.5GB, the kernel doesn’t kill it immediately; instead, it starts aggressively throttling the application’s performance and forcefully reclaiming cache, giving the application a chance to recover before it hits the hard 2G execution wall.
Step 4: Limiting Temporary Command-Line Processes (systemd-run)
You do not need to create a permanent systemd service file just to use cgroups. If you want to run a massive, one-off tar command to compress a directory, but you want to ensure it doesn’t starve the server of I/O or CPU, you can launch the command dynamically inside a temporary (transient) cgroup using systemd-run.
sudo systemd-run --scope -p CPUQuota=20% -p MemoryMax=1G tar -czvf archive.tar.gz /var/log/
Decoding the Flags:
--scope: Tells systemd to run the command in the foreground (so you can see the output), but still wrap it in a cgroup.-p CPUQuota=20%: Injects the property limit on the fly.
The tar command will execute, but it will be physically incapable of exceeding 20% CPU or 1GB of RAM. When the command finishes, systemd automatically destroys the temporary cgroup.
Step 5: Verifying the Enforcement
To verify that the kernel is actually enforcing the limits, you can interrogate the cgroup filesystem directly. Cgroups are represented as virtual files mounted at /sys/fs/cgroup/.
If you set a 2GB limit on experimental-app.service, you can read the kernel’s configuration file for that specific service:
cat /sys/fs/cgroup/system.slice/experimental-app.service/memory.max
The output will be the exact byte-value equivalent of 2GB, proving that the hardware-level restriction is actively in place.
Conclusion
Relying on software developers to write perfectly optimized, memory-safe code is a dangerous operational strategy. By leveraging Ubuntu’s systemd cgroup architecture, system administrators can build invisible, hardware-level sandboxes around unstable applications, guaranteeing that no rogue process can ever compromise the stability of the core operating system.