How to Configure Custom Hierarchical Slices in Ubuntu Using systemd.slice

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.

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.