Back to posts
Post

How to Limit Memory and IO for systemd Services Using Slices

Learn to isolate critical services with systemd slices, MemoryMax, and IOWeight to prevent resource starvation on Linux servers.

Sistemsystemdcgroupresource control

If you're running multiple services on a single Linux host and one starts consuming too much memory or disk IO, it can drag down everything else. I've seen this happen on busy web servers where a misbehaving backup job or log processor starves the frontend of resources. The fix isn't always more hardware — often it's better isolation. In my environment, I use systemd slices combined with MemoryMax and IOWeight to enforce hard boundaries. This keeps critical services responsive even when others misbehave. You don't need containers or VMs for basic isolation; systemd's built-in cgroup v2 controls are enough for many cases.

Why Use systemd Slices for Resource Control

Slices are systemd's way of grouping services into hierarchies that share cgroup limits. Instead of applying limits to each service individually, you can place them in a slice and manage the group as a unit. This is especially useful when you have a set of lower-priority background jobs that should never interfere with core services like a web server or database. I typically create a slice called system-minimal.slice for essential services and another like batch-low.slice for cron jobs or backups. By assigning services to these slices, I enforce policies at the group level — simpler and more scalable than per-service tuning.

Creating a Custom Slice with Memory and IO Limits

First, create a slice unit file. I put mine in /etc/systemd/system/ to keep it persistent across reboots. Here's an example for a low-priority batch slice:

# /etc/systemd/system/batch-low.slice
[Slice]
MemoryMax=2G
IOWeight=100

MemoryMax=2G caps the total memory used by all services in this slice at 2 gigabytes. If they try to exceed it, the OOM killer will start terminating processes within the slice — but not outside it. IOWeight=100 sets their relative IO priority; the default is 100, so this means they get baseline access. To really suppress them, I often drop this to 50 or even 10 when they're running alongside latency-sensitive workloads.

After saving the file, reload systemd and start the slice:

sudo systemctl daemon-reload
sudo systemctl start batch-low.slice

You can verify it's active with systemctl status batch-low.slice.

Assigning Services to the Slice

Now move your services into the slice. For a service like my-backup.service, edit its unit file and add:

[Service]
Slice=batch-low.slice

Reload and restart:

sudo systemctl daemon-reload
sudo systemctl restart my-backup.service

To confirm it's in the right slice, run:

systemctl show my-backup.service -p Slice

You should see Slice=batch-low.slice. You can also check the cgroup path directly: cat /proc/$(pgrep -f my-backup)/cgroup — it should show the slice hierarchy under /sys/fs/cgroup/.

Monitoring and Adjusting Limits

I use systemd-cgtop to get a real-time view of resource usage per slice and service. It's like top but for cgroups:

sudo systemd-cgtop

Watch the MEM% and IO% columns to see if your limits are being hit. If services in batch-low.slice are consistently stalled but not using their full memory allowance, the bottleneck might be IO — try lowering IOWeight further. Conversely, if they're getting OOM killed too often, increase MemoryMax or optimize the service itself.

For logging, I check journal entries for the slice:

sudo journalctl -u batch-low.slice --since "1 hour ago"

This helps correlate limit breaches with service behavior.

Important Caveats and Best Practices

First, these limits only work on cgroup v2. Most modern distros use it by default, but verify with mount | grep cgroup2. If you see cgroup v1 hierarchies, you may need to enable unified mode — though that's beyond this scope.

Second, avoid setting limits too tightly on essential services. I once accidentally put sshd.slice under a low-memory slice and locked myself out during a spike. Now I only apply strict limits to clearly non-critical, restart-safe workloads.

Third, combine this with other isolations. As I mentioned before in my post about using systemd-nspawn for risky command testing, layers help — but slices are great for baseline service hygiene without added complexity.

Finally, document your slice structure. I keep a simple markdown file in /etc/systemd/slice-policy.md listing each slice, its purpose, and its limits. It saves confusion during handoffs or audits.

When to Reach for More

Slices and basic cgroup limits solve 80% of my resource contention issues. But if you need per-device IO throttling (MaxBandwidth) or real-time CPU guarantees, look into full cgroup v2 configuration or consider systemd's Delegate= feature for nested containers. For most bare-metal or VM workloads though, a well-designed slice hierarchy with MemoryMax and IOWeight gives you predictable performance without the overhead.

It’s not flashy, but in production, quiet reliability beats flashy fixes every time.


Cover image: [email protected] · CC0 (Openverse / kamu malı) · https://www.thingiverse.com/thing:712213