Modern Linux servers and multi-tenant hosting environments run dozens of background daemons, user sessions, database engines, and container runtimes simultaneously. Under the Linux unified control groups architecture (cgroups v2), systemd manages all processes inside a hierarchical tree of units. While individual .service and .scope units can define specific CPU and memory limits, managing resource allocations for groups of related services individually creates management drift and uneven resource contention.
To establish clean multi-tier resource partitioning, systemd provides slice units (.slice). Slices do not execute code directly; instead, they organise the cgroup hierarchy, grouping services and scopes under parent resource umbrellas. By attaching multiple systemd services to a dedicated slice, administrators can enforce collective CPU weights, hard memory ceilings, and I/O limits across an entire workload tier (such as isolating all web workers from database engines or restricting batch compilation jobs). This guide explains how systemd slices function under cgroups v2, how to design custom hierarchical slice configurations in Ubuntu, and how to verify real-time resource throttling.
Understanding systemd Slice Architecture and Default Topologies
By default, systemd boots with a predefined three-tier slice hierarchy positioned directly beneath the root cgroup:
- -.slice (Root Slice): The root of the entire cgroup tree representing 100% of host compute, memory, and I/O bandwidth.
- system.slice: Houses standard operating system daemons, system services, and background background tasks (e.g. systemd-journald, sshd, rsyslog, cron).
- user.slice: Contains all interactive user login sessions, graphical desktop environments, and rootless user manager instances ([email protected]).
- machine.slice: Created automatically when running systemd-nspawn containers or libvirt virtual machines to isolate virtualised workloads.
Systemd supports nested hierarchical naming using dashes (-). For example, creating a slice named workload-databases.slice establishes a child cgroup inside workload.slice. If workload.slice is restricted to 16 GB of RAM, all child services inside workload-databases.slice and workload-api.slice collectively share that 16 GB boundary.
Verifying cgroups v2 Unified Hierarchy on Ubuntu
Ubuntu 22.04 LTS and 24.04 LTS mount the unified cgroups v2 hierarchy by default. Before designing slice units, confirm that cgroups v2 is active and that controllers are enabled:
# Verify cgroup2 mount
mount | grep cgroup2
# Inspect root available controllers
cat /sys/fs/cgroup/cgroup.controllers
The controller list should output support for core resource subsystems:
cpuset cpu io memory pids
To inspect your running server’s live slice hierarchy, use systemd-cgls:
systemd-cgls --slice
This command outputs an ASCII tree showing which services reside under system.slice, user.slice, and existing sub-slices.
Creating a Custom Slice Unit File
Custom slice unit definitions must reside in /etc/systemd/system/ with a .slice filename extension. In this scenario, we will construct a production application slice named production.slice designed to guarantee CPU priority while preventing memory starvation.
Create the unit file using a text editor:
sudo nano /etc/systemd/system/production.slice
Define the slice configuration with cgroups v2 resource controllers:
[Unit]
Description=Production Workloads Resource Slice
DefaultDependencies=no
Before=slices.target
[Slice]
# CPU scheduling weight (range 1-10000, default 100)
CPUWeight=500
# High memory watermark: triggers background memory reclamation/paging
MemoryHigh=6G
# Absolute hard memory limit: triggers OOM killer if exceeded
MemoryMax=8G
# Memory guarantee: protected from OOM reclamation during host contention
MemoryLow=2G
# Task process ID limit to prevent fork bombs
TasksMax=2048
# I/O scheduling weight (range 1-10000, default 100)
IOWeight=400
Save and close the file. Key configuration parameters include:
CPUWeight=500: Assigns five times more CPU scheduling cycles relative to the default weight (100) when the CPU is fully saturated.MemoryHigh=6G: Once all processes in this slice collectively consume more than 6 GB, the Linux kernel proactively throttles allocation speeds and aggressively reclaims page cache.MemoryMax=8G: The absolute maximum memory footprint allowed. Any allocation attempt beyond 8 GB forces kernel memory reclamation or terminates offending processes within the slice.TasksMax=2048: Prevents rogue multi-threaded applications from exhausting the system thread table.
Creating Nested Sub-Slices for Multi-Tier Workloads
To divide resources inside production.slice between backend API workers and database instances, create hierarchical child slices using hyphenated notation.
Create a child slice for database services:
sudo nano /etc/systemd/system/production-databases.slice
Configure the child unit:
[Unit]
Description=Production Database Sub-Slice
DefaultDependencies=no
Before=slices.target
[Slice]
# Sub-slice inherits parent boundaries and receives higher internal priority
CPUWeight=800
MemoryHigh=4G
MemoryMax=6G
Create a second child slice for API web services:
sudo nano /etc/systemd/system/production-web.slice
[Unit]
Description=Production Web API Sub-Slice
DefaultDependencies=no
Before=slices.target
[Slice]
CPUWeight=200
MemoryHigh=2G
MemoryMax=3G
Because these slices are named production-databases.slice and production-web.slice, systemd automatically places them inside production.slice, ensuring their combined consumption never exceeds the parent’s 8 GB MemoryMax ceiling.
Assigning Services to Custom Slices
To assign a systemd service to a custom slice, use the Slice= directive inside the service’s unit file, or apply a non-destructive drop-in override.
Applying Slices via Drop-In Overrides
To assign an existing service (e.g. nginx.service) to production-web.slice without editing packaged distribution files, create a drop-in configuration using systemctl edit:
sudo systemctl edit nginx.service
Enter the [Service] section with the target slice assignment:
[Service]
Slice=production-web.slice
Save and exit. Similarly, assign your PostgreSQL or MariaDB service to the database slice:
sudo systemctl edit postgresql.service
[Service]
Slice=production-databases.slice
Reloading and Activating Slices
Reload systemd manager configurations to parse new slice unit definitions and restart modified services:
sudo systemctl daemon-reload
sudo systemctl restart nginx.service postgresql.service
Monitoring Slice Resource Usage in Real Time
To monitor real-time CPU, memory, and I/O consumption across custom slices, use systemd-cgtop:
systemd-cgtop
To view only slice-level aggregates, press m to sort by memory or c to sort by CPU utilization:
Control Group Tasks %CPU Memory Input/s Output/s
/ 1482 12.4 7.2G - -
/production.slice 312 8.8 3.4G - -
/production.slice/production-databases.slice 94 6.2 2.6G - -
/production.slice/production-web.slice 218 2.6 820.4M - -
/system.slice 814 3.1 2.8G - -
/user.slice 356 0.5 1.0G - -
To inspect static and runtime properties of a slice unit directly, use systemctl show:
systemctl show production.slice -p MemoryCurrent -p MemoryMax -p CPUWeight -p TasksCurrent
PowerShell or bash scripts can parse these exact byte counts and task tallies to trigger external monitoring alerts before memory limits cause application throttling.