Skip to main content

cgroups Resource Isolation for Shared Hosting: How Per-Account CPU, RAM, IO and Process Limits Work

Updated 10 min readMarkdown
cgroups Resource Isolation for Shared Hosting: How Per-Account CPU, RAM, IO and Process Limits Work

Every shared hosting provider has seen the "noisy neighbour" problem. One account runs a badly written import script, a WordPress cron storm or a crypto miner from a hacked plugin, and suddenly every site on the server is slow. Customers who did nothing wrong open tickets, and you spend the evening finding the culprit in top.

The fix is resource isolation: giving every hosting account hard limits on CPU, memory, disk IO and processes, so a single account can only ever use its own share. On Linux, the kernel feature behind this is control groups (cgroups).

This article explains:

  • what cgroups are and how CPU, memory, IO and process limits work under cgroup v2,
  • which limits matter on a shared hosting server, and which aren't handled by cgroups at all,
  • the extra layers you need beyond cgroups (filesystem jails, per-account PHP pools, database limits),
  • how to choose sensible per-package limits.

Along the way I'll show how this is packaged in HPanel, the self-hosted hosting control panel I work on, which builds per-account cgroups isolation into the panel itself.

Disclosure: I'm on the HPanel team. HPanel specifics come from the public features page. The cgroup v2 details are standard Linux kernel behaviour.


What cgroups actually are

A control group is a set of processes that the kernel accounts for and limits together. Under cgroup v2 (the unified hierarchy used by current Ubuntu LTS releases), cgroups appear as directories under /sys/fs/cgroup. Each directory exposes small files called interface files: you write a value to set a limit and read a file to see usage.

On a systemd-based server you can look at the tree with:

systemd-cgls          # the cgroup tree
systemd-cgtop         # live usage per cgroup, like top
cat /sys/fs/cgroup/cgroup.controllers   # which controllers are available

For hosting, the idea is simple: put every process belonging to one hosting account (PHP-FPM workers, cron jobs, SSH sessions, Node or Python apps) into that account's cgroup, then set limits on that cgroup. The kernel enforces them, so no cooperation from the customer's code is needed.


The four limits that matter most

1. CPU: cpu.max

cpu.max takes two numbers, a quota and a period in microseconds:

# allow 1 full CPU: 100 ms of CPU time per 100 ms period
echo "100000 100000" > /sys/fs/cgroup/<account>/cpu.max

# allow 2 CPUs
echo "200000 100000" > /sys/fs/cgroup/<account>/cpu.max

When the account uses up its quota within a period, its processes are throttled until the next period. They slow down instead of being killed. That's exactly what you want for a busy site: it gets slower, but the rest of the server doesn't notice.

There's also cpu.weight, a relative share used when the CPU is contended. Hard caps (cpu.max) give predictability. Weights give fairness without wasting idle CPU. Many setups use both.

2. Memory: memory.max and memory.high

echo 1G > /sys/fs/cgroup/<account>/memory.high   # start reclaiming/throttling here
echo 2G > /sys/fs/cgroup/<account>/memory.max    # hard ceiling
  • memory.high is a soft limit. Above it the kernel reclaims memory aggressively and slows the group down.
  • memory.max is the hard limit. If the group can't get back under it, the kernel's OOM killer acts inside that cgroup only. The account's own process dies, not your MariaDB server.

That last point is the whole value of memory isolation. Without it, a runaway PHP process in one account can push the entire server into swap or trigger the OOM killer on something critical.

3. Disk IO: io.max

# limit reads and writes on device 8:0 to 10 MB/s and 1000 IOPS
echo "8:0 rbps=10485760 wbps=10485760 riops=1000 wiops=1000" \
  > /sys/fs/cgroup/<account>/io.max

IO is the limit people forget. On SSD and NVMe servers, CPU is rarely the first bottleneck. More often it's a backup job, a big tar, or a plugin hammering the disk that makes every other site's database queries crawl. Throughput (rbps/wbps) and operation rate (riops/wiops) limits prevent these "IO storms".

4. Processes: pids.max

echo 128 > /sys/fs/cgroup/<account>/pids.max

This caps how many processes and threads the account can have. It's the defence against fork bombs and runaway scripts that spawn workers in a loop. Once the limit is reached, new fork() calls fail inside that account, and the rest of the server keeps running.


What cgroups don't cover

Some per-account limits that hosting customers think of as "resources" are handled by other mechanisms:

Resource Mechanism
Disk space Filesystem quotas (block quotas per user)
Inodes (number of files) Filesystem inode quotas
Monthly bandwidth Traffic accounting (web server/log based) plus enforcement
Database load Database-level limits (queries/connections per user)
Seeing other users' files Permissions + a filesystem jail (chroot / mount namespaces)
PHP concurrency Per-account PHP-FPM pool size (pm.max_children)

A complete isolation setup combines cgroups with these. That's why "just enable cgroups" isn't the whole answer.


The other isolation layers

Filesystem jail

Even with CPU and memory capped, a compromised account shouldn't be able to see the rest of the server. A filesystem jail gives each user a restricted view, typically built with chroot and mount-based isolation, so they can't browse other users' home directories, system binaries or server configs.

Per-account PHP-FPM pools

If all sites share one PHP-FPM pool running as one user, both performance and security isolation are gone: one slow site exhausts the shared workers, and one webshell can read every site's files. A separate pool per account, running as that account's user, with its own pm.max_children, fixes both.

Database limits

The database server is shared by definition. A per-user MySQL governor limits queries, connections and updates per hour so that one account's heavy reporting query can't slow every other site's database.


How this looks in a hosting control panel

Setting all of this by hand across hundreds of accounts doesn't scale. You need packages, defaults and a UI that shows usage. In HPanel, isolation is part of the core (no extra kernel or extra license), and every hosting account gets hard limits on:

  • CPU: a specific CPU core count per account, hard-enforced,
  • RAM: hard memory limits per account,
  • IO: per-account disk IO limits in MB/s, to prevent IO storms,
  • Disk and inodes: hard disk quotas with real-time usage tracking and inode enforcement,
  • Bandwidth: per-account tracking and enforcement,
  • Processes: a maximum number of concurrent processes, to stop fork bombs and runaway scripts.

On top of that, HPanel provides a CageFS-style filesystem jail (users can't see other users' files, system binaries or server configs), per-account PHP-FPM pools with per-package PHP worker counts and priority, and a MySQL governor with per-user limits on queries, connections and updates per hour.

Per-account resource usage in the HPanel admin panel The HPanel Resource Manager: per-account limits and live usage.

Packages: limits as a product

Limits only make sense if they're tied to what the customer bought. In HPanel, limits are defined in hosting packages (disk, bandwidth, domains, subdomains, email accounts, databases, FTP, addon domains, CPU, RAM, IO, processes and PHP workers) and assigned to accounts. There are one-click presets to start from:

Preset CPU RAM
Light 1 CPU 512 MB
Standard 2 CPU 1 GB
Power 4 CPU 2 GB
Unlimited VPS-style VPS-style

Hosting packages with resource limits in HPanel Hosting packages define each account's limits.

Customers see their own usage in the user panel, which cuts down on "why is my site slow?" tickets.

Disk usage view in the HPanel user panel Disk usage breakdown in the user panel.

Performance view in the HPanel user panel Per-account performance and caching settings.


Choosing sensible limits

Some practical guidance for setting package limits:

  1. Start from your hardware, not your marketing. Add up what you're promising. If 50 accounts each get 2 GB RAM on a 32 GB server, you're relying on most of them being idle. Some overcommit is normal on shared hosting, but know your ratio.
  2. CPU caps should allow bursts. Page loads are spiky. A 1-CPU cap is usually fine for a small WordPress site. A 0.25-CPU cap makes every uncached request feel slow.
  3. Set memory.high below memory.max (when you control both) so accounts get throttled before they get OOM-killed.
  4. Don't forget IO. If you only cap CPU and RAM, the next incident will be disk-bound.
  5. Keep process limits generous but finite. Real sites need dozens of processes (PHP workers, cron, mail). Hundreds is usually fine. "Unlimited" is how fork bombs win.
  6. Match PHP workers to RAM. PHP-FPM max_children × average PHP memory per process must fit inside the account's memory limit, or the account will hit its own ceiling under load.
  7. Watch, then adjust. Use live usage data for a couple of weeks before tightening anything.

Debugging a throttled account

When a customer says "my site is slow", isolation changes the question from "what's wrong with the server?" to "is this account hitting its limits?". On a generic cgroup v2 system you can check directly:

cat /sys/fs/cgroup/<account>/cpu.stat        # nr_throttled, throttled_usec
cat /sys/fs/cgroup/<account>/memory.events   # high, max, oom_kill counters
cat /sys/fs/cgroup/<account>/io.stat         # bytes and ops per device
cat /sys/fs/cgroup/<account>/pids.current    # current process count

Rising nr_throttled means the CPU cap is binding. oom_kill events mean the memory limit is too low for the workload, or there's a leak. In a control panel, the same information should appear as per-account usage graphs, which is where you want your support team looking.


FAQ

Do cgroups slow down the server? The accounting overhead is small. cgroups are what systemd already uses to manage every service on a modern Ubuntu server. The "slowdown" people notice is the limit doing its job: an account that hits its CPU cap gets throttled.

Is cgroups isolation the same as running each account in a VM or container? No. Accounts still share one kernel and one set of system services. cgroups limit resources. Namespaces, chroot jails and permissions limit visibility. Together they give strong practical isolation for shared hosting, but not the hard boundary of separate virtual machines.

Can a customer raise their own limits? Not when the limits are set on the cgroup by root, which is the point. Customers change limits by moving to a bigger package.

What about Node.js, Python and Docker workloads? They need the same treatment. Long-running app processes are exactly the kind that can creep up in memory over days. In HPanel, Docker container CPU and memory limits come from the hosting package, and containers count against the user's disk quota, so app hosting follows the same rules as PHP sites.

Do I need a separate kernel or add-on for this? Not with cgroup v2 on current Ubuntu LTS releases. The controllers are in the standard kernel. What you need is the tooling to apply limits per account, consistently, and to show usage.


Wrapping up

Resource isolation turns shared hosting from "everyone suffers together" into "each account lives within what it bought". cgroups provide the kernel-level enforcement for CPU, memory, IO and processes. Filesystem quotas, a filesystem jail, per-account PHP pools and database limits complete the picture.

You can build all of it yourself. If you'd rather have it as the default on every account, HPanel includes per-account cgroups limits, a filesystem jail, per-account PHP-FPM pools and a MySQL governor in every plan, with one flat license per server and no per-account fees. The setup guide is in the documentation.

Ready to isolate every account on your server? Buy now.

About the author

  • Self-hosted hosting control panel for Ubuntu servers. Website: https://hpanel.net/ Pricing: https://hpanel.net/pricing.php

No comments yet. Be the first to comment.