Candid photograph of a Linux server terminal with a soft olive-green glow against a dark slate background, conveying a calm technical atmosphere.

Step-by-step guides for system administrators — covering command-line basics, web server setup, and preparation material for technical interviews.

Browse Tutorials

Managing System Resources with cgroups on CentOS

On a CentOS host running several daemons, a couple of NGINX workers, and a few containerised workloads, the kernel has to decide who gets CPU time, who holds memory, and which process can push disk I/O. Control groups, commonly shortened to cgroups, are the mechanism that turns those decisions from advisory to enforceable. For anyone running RHEL-family distributions in production, cgroups sit quietly underneath both systemd service management and the per-container limits that Podman and Docker apply.

Australian sysadmins encounter cgroups in fairly specific environments: providers such as NextDC and Macquarie Telecom hosting CentOS for government and ASX-listed tenants, mining operators running telemetry pipelines in Perth, and university research clusters at CSIRO or the University of Melbourne. In each of those settings, a runaway process inside one slice cannot be allowed to starve workloads in another. Cgroups convert that "should not happen" into "cannot happen", which is why the topic belongs alongside the file-permission basics covered in our Understanding Linux File Permissions and Umask guide. Both are foundational controls an admin needs to apply before handing the system to a tenant.

The rest of this walkthrough looks at how cgroups are exposed on CentOS, how to inspect what is consuming resources, and how to write limits that survive a reboot. The examples use systemd units and read-only commands so they can be applied directly to a server running CentOS Stream 9 or Rocky Linux 9.

cgroup versions and where the filesystem lives

CentOS 7 shipped with cgroup v1, which splits resources into separate hierarchies mounted under /sys/fs/cgroup, including cpu, memory, blkio, devices, and freezer. RHEL 8 and CentOS Stream 8 kept v1 available through the systemd.unified_cgroup_hierarchy=0 kernel argument, while CentOS Stream 9 and RHEL 9 default to cgroup v2, the unified hierarchy, where every controller lives under a single tree.

You can confirm which hierarchy your box uses with a quick read of the mount table. Running mount | grep cgroup on CentOS Stream 9 shows a single entry for cgroup2 on /sys/fs/cgroup type cgroup2, whereas an older CentOS 7 system lists multiple v1 controllers. Where a script has to handle both, address the v2 paths first and fall back to the legacy directories only when the kernel argument forces v1.

For most CentOS Stream 9 deployments in Australia, sticking with v2 simplifies things considerably. Systemd writes its accounting to the unified tree by default, and container runtimes such as Podman expect that layout. Maintaining two parallel hierarchies on one host leads to silent mismatches between systemd controls and the limits reported inside a container.

Inspecting resource consumption without extra packages

Systemd ships its own cgroup inspectors, so there is no need to install a third-party monitor to see what the system is doing. systemd-cgtop prints a top-like view of every active cgroup, sorted by CPU load by default, and the sort key can be switched between memory, I/O, and tasks. The wall-clock output reflects AEST on an Australian box, which matters when correlating spikes with local cron schedules rather than UTC.

For a structured view, systemd-cgls draws the tree from the root slice down to individual services. The output makes it easy to verify that a process launched with systemd-run actually landed in the slice you intended. To see the raw counters under v2, cat /sys/fs/cgroup/<slice>/<service>/memory.current returns the live byte usage for a given service.

A few commands keep the inspection loop short:

When a longer-form dashboard is needed, the ccjeng tutorials walk through pulling these counters into custom graphs running on a CentOS VM.

Applying CPU and memory limits through systemd units

The cleanest way to constrain a service on CentOS is through a drop-in override rather than editing the unit file shipped by the package. Place a file such as /etc/systemd/system/telemetry.service.d/10-limits.conf and reload with systemctl daemon-reload. Directives inside the file do most of the heavy lifting:

These directives only take effect after systemctl restart telemetry.service. A common mistake in Australian hosting environments is applying the override and then running systemctl start, which leaves the old cgroup in place with the old accounting. Always restart, and then verify with systemd-cgtop that the new ceiling is visible.

For ad-hoc limits, systemd-run --scope --property=CPUQuota=50% --property=MemoryMax=512M /usr/bin/process-name creates a transient scope that disappears when the command exits. That pattern is a quick way to throttle a one-off build job running in a Canberra development VM without committing the limit to a unit file.

Throttling block I/O on the unified hierarchy

Memory and CPU are the obvious constraints, but a chatty logger or a backup script can still saturate the disk and drag database latency through the floor. Cgroup v2 exposes io.max and io.weight directly inside /sys/fs/cgroup/<slice>/. Reading the current file shows which device a slice is bound to, a useful sanity check before applying any throttle.

Writing limits goes through a systemd drop-in using IOReadOnlyMaxBandwidth=, IOWriteMaxBandwidth=, and the older BlockIOWeight= directive. The exact names differ slightly between CentOS Stream 8 and Stream 9, so man systemd.resource-control is worth running before committing a value. A reasonable starting point for an archive runner is IOWeight=100 combined with IOReadOnlyMaxBandwidth=50M, leaving the bulk of device bandwidth for the database service.

After the file is in place, restart the service, then read the same values back from the cgroup directory to confirm systemd wrote them where expected. If the numbers do not match, the system is most likely booted into a controller set that ignores the directive, a mismatch worth chasing before any further tuning.

Persistent slices for shared tenants

When more than one team shares a CentOS host, such as a colocated appliance in a Melbourne point of presence, the cleanest pattern is to define a top-level slice per tenant and let each team drop services beneath it. Create /etc/systemd/system/analytics.slice with Before=slices.target and a [Slice] section containing CPUAccounting=yes and MemoryAccounting=yes. Teams then attach units to the slice with Slice=analytics.slice in their drop-ins.

Set CPUWeight= and MemoryHigh= on the slice itself rather than on each unit, so the ceiling is shared fairly across the team. After deployment, run systemd-cgls analytics.slice to confirm units actually attached, instead of drifting into the system slice unnoticed. The final verification is to inspect /sys/fs/cgroup/analytics.slice/ directly. If cpu.max shows the intended ceiling, the configuration is live.

Once the slice is stable, feed the slice names defined above into the Podman or Kubernetes node selector on this host, so container resource requests map onto the same systemd limits rather than reintroducing a parallel accounting path.