...

Server Resource Isolation with cgroups in hosting: Ultimate Guide

In this guide to cgroups Hosting I will show you specifically how I isolate server resources with Linux control groups so that „noisy neighbors“ do not slow down any services. You will learn how I limit, prioritize and reliably monitor CPU, RAM, block I/O and network per website, container or user - in a practical and implementable way.

Key points

The following key aspects will guide you through the most important decisions and steps.

  • Insulation: Separate processes cleanly and tame neighbors
  • ControlLimit CPU, RAM, I/O and devices in a targeted manner
  • Priority: Weight and protect premium services
  • TransparencyMeasure load, use alarms and trends
  • Upgrade: From v1 to v2 for clear management

How cgroups disconnect server resources

Control groups organize processes into groups and connect these groups to resource drivers, which allows me to Resources per group. On a shared server, this prevents a single website from consuming CPU time or filling the memory to the brim. To do this, I set up hierarchies in which parent groups specify upper limits that are inherited by child groups. This keeps the load distribution consistent and I keep bottlenecks at bay. In particular, I noticeably alleviate the „noisy neighbor“ problem because strong spikes run in isolated tracks.

Controller and file system: the tools of the trade

The practical work starts in the cgroup file system under /sys/fs/cgroup, where I create groups and set limits, i.e. the concrete Control system take care of. I use controllers such as cpu, memory, blkio, cpuset and devices to allocate time slices, cover RAM, slow down I/O, pin cores or lock devices. I combine these building blocks depending on the application: for example, memory-hungry apps with hard RAM limits, build jobs with CPU weights and databases with I/O bandwidths. A clear naming scheme is important so that I can quickly find the groups again later. This keeps the administration manageable and changes are not lost sight of.

Subsystem Purpose
cpu / cpuacct Allocate CPU time, Weights and set quotas
memory Limit RAM, avoid OOM kills
blkio Throttle block I/O, control read/write rate
cpuset Assign CPU cores and NUMA nodes
devices Allow or block device access
net_cls / net_prio Mark and prioritize network classes

cgroups v1 vs. v2 in hosting

The older v1 separates controllers into several trees, which I quickly found to be confusing in large setups, so I decided to use the Changeover to v2. cgroups v2 bundles everything in a clear tree and thus simplifies administration, debugging and inheritance. In addition, cpu.max, cpu.weight and memory.max in v2 provide a coherent set of levers that work well together. Container orchestrators also prefer to use v2 because the semantics are more consistent. For hosting environments with many clients, v2 is therefore the leaner and more reliable choice.

Fine control in cgroups v2: io, memory, pids and PSI

In v2, I activate controllers explicitly per subtree and thus gain more control over inheritance. Only when I allow them in the parent node can I use them in child groups - this prevents uncontrolled growth and ensures clean policies.

# Activate controller in root node for children
echo "+cpu +io +memory +pids" > /sys/fs/cgroup/cgroup.subtree_control

# Create subgroup and use as workspace
mkdir /sys/fs/cgroup/prod-web

For I/O, I use the io controller in v2 (instead of blkio). I set bandwidth or IOPS limits per device via major:minor specifications. This ensures that logs, indexes or backups do not clog up the disk.

# Device for /dev/sda (8:0) limit to 50 MiB/s read, 20 MiB/s write
echo "8:0 rbps=50M wbps=20M" > /sys/fs/cgroup/prod-web/io.max
# Allocate weights relatively (instead of hard cap, 100-10000, default 100)
echo 500 > /sys/fs/cgroup/prod-web/io.weight

I set the memory in two stages: memory.high slows things down early, memory.max is a hard stop. I also deactivate swap for critical services so that latencies don't explode.

# Soft brake (soft limit) and hard cover
echo $((400*1024*1024)) > /sys/fs/cgroup/prod-web/memory.high
echo $((512*1024*1024)) > /sys/fs/cgroup/prod-web/memory.max

# prohibit swap (0) or limit (e.g. 128 MiB)
echo 0 > /sys/fs/cgroup/prod-web/memory.swap.max

I use the pids controller to prevent fork storms and maintain an upper limit for processes and threads per client - this is effective protection against misconfiguration and misuse in shared environments.

# Maximum 512 processes/threads in the group
echo 512 > /sys/fs/cgroup/prod-web/pids.max

For diagnostics, I use cgroup.events and PSI (Pressure Stall Information) for each group. PSI shows me whether CPU, memory or I/O are regularly running low and causing waiting times. This is more valuable than pure utilization because it makes bottlenecks visible before users notice them.

# Read out events and pressures
cat /sys/fs/cgroup/prod-web/cgroup.events
cat /sys/fs/cgroup/prod-web/cpu.pressure
cat /sys/fs/cgroup/prod-web/memory.pressure
cat /sys/fs/cgroup/prod-web/io.pressure

In OOM situations, I bundle kills specifically with memory.oom.group so that related processes (e.g. worker + helper) are terminated consistently instead of putting the system into a partial paralysis state. For maintenance windows, I freeze groups briefly with cgroup.freeze and then unfreeze them again - useful for database migrations or atomic rollouts.

# OOM behavior group-wide
echo 1 > /sys/fs/cgroup/prod-web/memory.oom.group

# Pause / resume group
echo 1 > /sys/fs/cgroup/prod-web/cgroup.freeze
echo 0 > /sys/fs/cgroup/prod-web/cgroup.freeze

Step-by-step: Setting limits under Linux

On servers with the current kernel, I activate cgroup v2 and then create groups with suitable upper limits, whereby I can use the Control directly in the system. The following commands show a minimalist example with CPU and RAM limit. I select quotas conservatively, monitor the load and adjust in iterations. In this way, I keep response times low without unnecessarily cutting useful burst phases. The implementation remains close to the kernel and works independently of panel software.

# mount cgroup v2 (if not automatically via systemd)
mount -t cgroup2 none /sys/fs/cgroup

Create # group
mkdir /sys/fs/cgroup/prod-web

Assign # process (example: PID 1234) to the group
echo 1234 > /sys/fs/cgroup/prod-web/cgroup.procs

# CPU quota: 100 ms of 200 ms => 50%
echo "100000 200000" > /sys/fs/cgroup/prod-web/cpu.max

# RAM hard limit: 512 MiB
echo $((512*1024*1024)) > /sys/fs/cgroup/prod-web/memory.max

Systemd integration and persistent slices

In everyday life, I let systemd manage the cgroup tree. So limits remain persistent and are documented transparently for each service. I work with slices (system.slice, user.slice, machine.slice) and define my own hierarchies for clients or roles. I use drop-in files to set weights and caps without having to write manually in /sys/fs/cgroup.

# Example: create your own slice for web services
cat > /etc/systemd/system/web.slice < /etc/systemd/system/php-fpm.service.d/10-slice.conf <<'EOF'
[Service]
Slice=web.slice
EOF

systemctl daemon-reload
systemctl restart php-fpm.service

For ad hoc tests, I start processes in scopes without writing unit files. This is suitable for simulating load peaks or trying out limits for a short time.

# Briefly set resources and start process under control
systemd-run --scope -p CPUWeight=200 -p MemoryMax=512M \
  -p IOReadBandwidthMax=/dev/sda:50M -p IOWriteBandwidthMax=/dev/sda:20M \
  stress-ng --vm 1 --vm-bytes 300M --cpu 1

Container runtimes manage their own slices (machine.slice) under systemd. Delegation is important here: I allow the runtime service to manage the subtree (delegate) so that pods/containers are cleanly isolated. In this way, host and container policies do not overlap.

Use container limits safely

In container environments, cgroups act as invisible guard rails that I set via runtime parameters and thus Container to fair limits. Docker maps -cpus, -memory and -blkio options directly to cgroups, for example, while Kubernetes translates requests and limits into v2 parameters. Consistency is crucial: limits must match the real load so that pods and containers do not throttle unnecessarily or cause OOM errors. I keep productive services tighter, while build or test jobs only receive more budget if necessary. This keeps deployments predictable and prevents rollouts from stalling.

Avoid shared hosting and noisy neighbors

In shared environments, I set limits at package or subscription level so that I can Fairness between clients. Panels such as Plesk make this easier with a Cgroups manager that allocates CPU, RAM and I/O per subscription and displays them visually. I also activate notifications to immediately recognize and react to load peaks. If you want to compare Tenant-Isolation in detail, you can take a look at Tenant insulation throw. This means that all websites remain responsive, even if individual customers generate more traffic at times.

Setting limits correctly: cgroups vs. ulimits

cgroups cap real usage, while ulimits is mainly per-process or shell hard limits, which is why I use Combinations specifically. For CPU, RAM and I/O I set clear cgroups, for open files or processes I additionally control via ulimit. This prevents file descriptor bottlenecks and still keeps overall usage in check. If you want to refresh the system-related limits, you can find a good overview at Ulimits in hosting. Both levels together provide the finest adjustment screws for fair client separation.

Monitoring and alerting

Without measurement, I make decisions blindly, so I run metrics permanently and set Thresholds for alarms. Tools such as systemd-cgtop, ps, pidstat or Prometheus-Exporter show me live which group is currently using resources. In panels, I link the cgroups to dashboards that mark threshold values and make trends visible. Email or chat alerts inform me when groups exceed limits or throttle frequently. This allows me to identify bottlenecks early on and adjust limits, expand hardware or optimize code paths.

In-depth monitoring: key figures, PSI and meaningful alarms

I not only monitor „usage“, but also „pressure“. In cgroups v2 I read cpu.stat (including throttled_us), memory.current, memory.high, memory.events, io.stat and the pressure files. I use these to build alarms that react early to resource shortages without being annoying during short peaks.

  • CPU: Alert if throttled_us increases permanently and latencies deteriorate at the same time. Then I increase CPUWeight or loosen cpu.max.
  • Memory: memory.current near memory.high is a warning signal. If memory.events frequently reports „high“, I increase high or optimize caches.
  • I/O: io.stat shows rbps/wbps and waiting times. I correct permanent throttling via io.weight or dedicated devices.
  • PSI: Persistent „some“/„full“ pressure is an indicator that workloads are regularly waiting for resources. I use this for capacity planning.

Best practices for clean configuration

I always start with conservative values, because lids that are too sharp at peak times Performance costs. I then specifically load the service with benchmarks such as ab, siege or wrk to gradually increase quotas. For multi-app hosts, I arrange groups in slices so that important services are given priority without depriving others of everything. I set I/O limits so that short peaks slip through, but longer phases are slowed down. Regular reviews prevent limits from becoming obsolete while load profiles change.

Migration from v1 to v2: this is how I proceed

I plan the changeover like a regular infrastructure upgrade. First, I check whether the kernel and systemd v2 are enabled by default. If necessary, I start with suitable boot parameters and validate that the unified tree is active. I then test all integrations (panels, agents, backups, container runtime) in a staging environment.

  • Detection: mount | grep cgroup2 or systemd-cgls shows me whether v2 is running.
  • Boot parameters: If required, I set cgroup_no_v1=all or unified options so that only v2 is active.
  • Controller mapping: blkio becomes io; I replace some v1 features (net_cls/prio) with traffic control with cgroup or BPF classifiers.
  • Migrate policies: Use weights instead of rigid quotas, introduce memory.high, limit swap separately.
  • Customize monitoring: Transfer new paths and fields (cgroup.events, cpu.stat) to dashboards.

Add process isolation

cgroups solve the issue of resources, but for system access I additionally separate Name spaces and file views. Chroot, CageFS, namespaces and jails seal off paths, kernel objects and devices so that clients cannot reach each other. This layer of protection is a useful addition to limits because it reduces the damage radius and attack surface. You can find a concentrated overview of the most important variants here: Process isolation. In combination with cgroups, a clean client separation is created for hosting setups of any size.

Practical scenarios and tuning

During traffic peaks in CMS setups, I give the CPU some short-term breathing space, but keep RAM hard so that I can Security against OOM failures. For data-intensive stores, I regulate blkio so that indexing does not slow down all other read processes. I pin analytics or worker processes to a few cores with cpuset so that web workers can respond undisturbed on the other cores. I move background jobs to groups with lower CPU weights so that frontend requests remain fluid. For dedicated customers, I allow memory.min to secure a small guaranteed RAM base for a premium app.

Troubleshooting and typical stumbling blocks

Some error patterns are repeated. I keep an eye on the following points to save time when troubleshooting:

  • CPU quotas too hard: Permanent throttling increases latency. Better to work with cpu.weight and cpu.max only as a safety belt.
  • Memory pressure without OOM: memory.high limits effectively, but if the page cache is capped too much, I/O latencies increase. Fine-tune and trim caches selectively.
  • Swap effects: Too much swap makes systems sluggish. Therefore, operate critical services with memory.swap.max=0, but secure the entire system with a small buffer.
  • Subtrees forgotten: Without an entry in cgroup.subtree_control, child limits do not apply. Always activate the controller in the parent node first.
  • Wrong group: Processes sometimes end up in the wrong slice. I check with systemd-cgls and correct service unit options (Slice=, Delegate=).
  • pids.max too low: Daemon with many worker threads fails silently. Select a generous buffer and track in monitoring.
  • I/O limits per device: For RAID/LVM, use the correct major:minor or set limits to the visible block devices that the workload actually uses.
  • Network prioritization: net_cls/prio are v1 legacy. In v2, I rely on traffic control with cgroup or BPF classifiers to control traffic flows.

Roles, profiles and fairness models

I like to work with clear service profiles that I store as templates and roll out automatically:

  • Premium (Gold): High CPU and I/O weights, memory.min for guaranteed base, hard memory.max limit with sufficient reserve.
  • Standard (silver): Medium weights, moderate io.weight, memory.high slightly below peak to avoid cache sprawl.
  • Background (Bronze): Low CPU/I/O weights, strict cpu.max for decoupling interactive workloads.

I also reserve cores and RAM for the host and central infrastructure (monitoring, logging, backup). In this way, I prevent clients from swallowing the system overhead. For NUMA hosts, I use cpuset to keep memory local to the cores used - this reduces latency peaks for memory-intensive services.

Brief summary

With cgroups I set clear guard rails for CPU, RAM, I/O and network, which allows me to Fairness between services and mitigate bottlenecks. The standardized architecture of cgroups v2 makes planning, operation and troubleshooting easier for me compared to v1. In containers, shared hosting and mixed environments, I can keep „noisy neighbors“ in check and protect critical workloads. Monitoring and useful alarms give me early signals when limits no longer match load profiles. If you combine cgroups with process isolation, ulimits and clean tuning, you can build a reliable hosting platform that performs consistently and treats clients fairly.

Current articles