How to Control Cpu, Memory, and I/o Resources Per Service Using Cgroups V2 and systemd Slices on Ubuntu Server
Learning how to control CPU, memory, and I/O resources per service using cgroups v2 and systemd slices on Ubuntu Server gives you precise control over what each process can consume. Without resource limits, a single misbehaving service can starve your entire system. A runaway PHP-FPM worker, a memory-hungry MySQL query, or a backup job hammering disk I/O can all bring a production server to its knees. This tutorial shows you how to prevent that. You’ll learn what cgroups v2 is, how systemd slices map to cgroup hierarchies, and how to set hard limits on CPU, memory, and I/O for individual services. By the end, you’ll have a repeatable, systemd-native approach to resource isolation that works on Ubuntu 22.04 and later.
Prerequisites and Requirements for Controlling Resources with cgroups v2
Before you start, make sure your environment meets these requirements.
Required setup:
- Ubuntu Server 22.04 or 24.04 (cgroups v2 is enabled by default)
- Root or sudo access
- At least one running systemd service to test with (nginx, mysql, or similar)
- Basic familiarity with systemd and the terminal
Estimated time: 30–45 minutes
First, confirm your kernel is using the unified cgroup v2 hierarchy. Run this command:
mount | grep cgroup2
You should see output like cgroup2 on /sys/fs/cgroup type cgroup2. If you see nothing, your system may still be using cgroups v1. You can force v2 by adding systemd.unified_cgroup_hierarchy=1 to your kernel parameters in /etc/default/grub, then running sudo update-grub and rebooting.
Also check your systemd version. It should be 245 or higher for full cgroups v2 support:
systemctl --version
Step-by-Step Guide to Control CPU, Memory, and I/O Resources Per Service
See also: How to Set Up Wordpress Ai Client Sdk with Multiple Providers in Wordpress 7.0
Follow these steps to apply resource controls to a systemd service using slices and cgroup directives.
Step 1: Understand systemd slices
Systemd organizes units into a tree of slices. The default slices are system.slice (for system services), user.slice (for user sessions), and machine.slice (for VMs and containers). Every service belongs to a slice. You can create custom slices to group services and apply shared limits.
Step 2: Create a custom slice for web services
Create a new slice unit file for your web-facing services:
sudo nano /etc/systemd/system/web.slice
Add this
[Unit]
Description=Web Services Slice
Before=slices.target
[Slice]
CPUQuota=60%
MemoryMax=2G
IOWeight=50
This limits all services in this slice to 60% total CPU, 2 GB of memory, and a relative I/O weight of 50. Save and close the file.
Step 3: Assign a service to your custom slice
Now assign an existing service, such as nginx, to the web.slice. Use a systemd drop-in file to avoid editing the original unit:
sudo mkdir -p /etc/systemd/system/nginx.service.d
sudo nano /etc/systemd/system/nginx.service.d/slice.conf
Add this:
[Service]
Slice=web.slice
Save the file. Reload systemd and restart nginx:
sudo systemctl daemon-reload
sudo systemctl restart nginx
Step 4: Set per-service CPU limits
You can apply CPU limits directly to a single service without a custom slice. Use another drop-in file:
sudo nano /etc/systemd/system/mysql.service.d/limits.conf
Add these directives:
[Service]
CPUQuota=40%
CPUWeight=50
CPUQuota sets a hard cap. CPUWeight sets relative priority when CPUs are contested. A weight of 100 is the default. Lower values get less CPU time under contention.
Step 5: Set per-service memory limits
Memory limits are critical. Without them, a service can consume all available RAM and trigger the OOM killer. Add memory limits to the same drop-in file:
[Service]
CPUQuota=40%
CPUWeight=50
MemoryMax=1G
MemorySwapMax=0
MemoryMax is a hard limit. The kernel will kill the service if it exceeds this. MemorySwapMax=0 disables swap for this service entirely. That’s useful for latency-sensitive workloads.
You can also set MemoryHigh as a soft limit. The kernel will throttle the service before it hits MemoryMax:
MemoryHigh=800M
MemoryMax=1G
Step 6: Set per-service I/O limits
I/O throttling prevents backup jobs or batch processes from saturating your disk. First, find your disk device name:
lsblk
Note the device, for example /dev/sda. Then add I/O limits to your drop-in file:
[Service]
IOReadBandwidthMax=/dev/sda 50M
IOWriteBandwidthMax=/dev/sda 50M
IOWeight=25
This caps read and write bandwidth at 50 MB/s each and sets a low I/O priority weight. The full list of systemd resource control directives is available in the official documentation.
Step 7: Reload and verify your settings
After making changes, reload systemd and restart the affected service:
sudo systemctl daemon-reload
sudo systemctl restart mysql
Verify that the cgroup limits are applied:
systemctl show mysql | grep -E 'CPU|Memory|IO|Slice'
You can also inspect the live cgroup values directly:
cat /sys/fs/cgroup/system.slice/mysql.service/memory.max
cat /sys/fs/cgroup/system.slice/mysql.service/cpu.max
Use systemd-cgtop to watch real-time resource usage per cgroup:
sudo systemd-cgtop
This gives you a live view similar to top, but organized by cgroup hierarchy. See the Ubuntu manpage for systemd-cgtop for all available options.
Troubleshooting Common Issues with cgroups v2 and systemd
Problem: Changes don’t take effect after reload
Make sure you ran sudo systemctl daemon-reload before restarting the service. Drop-in files are only read at reload time. If the service was already running, a restart is required.
Problem: CPUQuota has no effect
Check whether your kernel has the CPU controller enabled in cgroups v2:
cat /sys/fs/cgroup/cgroup.controllers
You should see cpu memory io listed. If cpu is missing, your kernel may not have it compiled in or the controller may be disabled.
Problem: MemoryMax is ignored
Some older kernels don’t enforce MemoryMax strictly. Make sure you’re running kernel 5.15 or higher. Check with uname -r.
Problem: I/O limits don’t apply to NVMe drives
Use the correct device path. NVMe drives appear as /dev/nvme0n1, not /dev/sda. Double-check with lsblk and update your drop-in file accordingly.
Warning: Setting MemoryMax too low will cause the kernel to kill your service. Always start with conservative values and monitor with systemd-cgtop before tightening limits in production.
Conclusion
You now know how to control CPU, memory, and I/O resources per service using cgroups v2 and systemd slices on Ubuntu Server. You created a custom slice, assigned services to it, and applied hard limits using drop-in configuration files. This approach is clean and version-controllable. It doesn’t require any third-party tools. The limits survive reboots and service restarts automatically. From here, you can explore MemoryLow and CPUWeight for soft guarantees, or group multiple services under shared slices for team-based or application
