CPU Bandwidth Control with cgroups v2: The Complete Practical Guide
Free Linux kernel development course lesson — limit, weight, and monitor CPU usage with cpu.max and cpu.weight on modern Linux
Intermediate
cgroups v2 only
100% Free
Priorities decide who runs first. They do not decide how much CPU anyone gets. Picture a shared Linux server with ten users: scheduling is fair between runnable threads, but nothing stops one user from spawning fifty CPU-hungry threads and effectively hogging the machine. Solving that problem is the job of CPU bandwidth control with cgroups, and it is the focus of this lesson in our free Linux kernel development course.
One big update before we start: older books teach cgroups v1 with files like cpu.cfs_quota_us. That world is gone. cgroup v1 has been deprecated across the ecosystem — systemd removed support for it entirely, and current distributions (RHEL 10, Ubuntu 26.04 LTS, recent Fedora, Arch) boot with a pure cgroups v2 unified hierarchy. Everything in this tutorial is cgroups v2, the only interface you should learn in 2026.
What You Will Learn
- Why fair scheduling alone cannot prevent CPU hogging, and what cgroups add
- The cgroups v2 unified hierarchy: one tree, one mount, one set of rules
- The CPU controller knobs:
cpu.max,cpu.weight, andcpu.stat - A hands-on lab: throttle a CPU burner to 25% of one core, live
- The clean way to do the same with systemd (
CPUQuota=,systemd-run) - How to verify throttling with
cpu.statandtop
Prerequisites
A Linux machine or VM with a modern distribution (kernel 5.x/6.x with systemd). Root access is needed for the manual sysfs experiments. Familiarity with the earlier scheduler lessons of this free embedded systems course helps but is not mandatory — this lesson stands on its own.
The Problem: Fair Between Threads Is Not Fair Between Users
The Linux scheduler (CFS historically, EEVDF since kernel 6.6) shares CPU time fairly between runnable threads. But fairness per thread is easy to game: whoever creates the most threads wins. Ten users on a server, and one of them launches a 50-thread compression farm — the scheduler dutifully gives each thread a fair share, meaning that one user now consumes over 80% of the machine.
What was needed was a way to allocate CPU (and memory, I/O, and more) to groups of tasks, with limits and proportions enforced by the kernel. Control groups — cgroups — are exactly that: a kernel feature that lets an administrator carve system resources into a hierarchy and attach resource controllers to each level.
cgroups v2: One Unified Hierarchy
The original cgroups design (v1) allowed a separate hierarchy per controller, which turned out to be confusing and inconsistent. cgroups v2 rebuilt the model around a single unified tree:
- One mount point:
/sys/fs/cgroup(filesystem typecgroup2). - All controllers (cpu, memory, io, pids, …) attach to the same tree.
- Processes live only in leaf cgroups; inner nodes distribute resources.
- Controllers are enabled per-subtree via the
cgroup.subtree_controlfile.
Check what your system is running:
# cgroup2 on /sys/fs/cgroup means pure v2 - the modern default
$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)
# Which controllers are available at the root?
$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc
| /sys/fs/cgroup (root cgroup) | ||
| ↓ | ||
| system.slice system services (sshd, cron…) |
user.slice login sessions per user |
mygroup our lab cgroup, cpu.max applied |
| ↓ resources flow top-down; limits at each level bound the whole subtree ↓ | ||
| Leaf cgroups hold the actual processes (cgroup.procs) | ||
The CPU Controller Knobs
| File | Purpose | Example |
|---|---|---|
cpu.max |
Hard ceiling: “quota period” in microseconds. The group may consume at most quota µs of CPU per period µs. | 25000 100000 = 25% of one CPU |
cpu.weight |
Proportional share under contention (1–10000, default 100). Replaces v1 cpu.shares. |
Weight 200 vs 100 = 2:1 split when both are busy |
cpu.stat |
Read-only accounting: usage, and how often/how long the group was throttled. | nr_throttled, throttled_usec |
cpu.max.burst |
Allows saving unused quota to absorb short spikes without throttling. | 20000 = up to 20 ms of banked burst |
The two knobs answer different questions. cpu.weight says “when everyone wants CPU, split it in this ratio” — it never wastes idle CPU. cpu.max says “never exceed this ceiling, even if the machine is idle” — ideal for multi-tenant fairness and predictable billing.
Hands-On Lab: Throttle a CPU Hog to 25%
Let us prove it works. First, a deliberately greedy little program:
// burn.c - spin forever, eating one full CPU
// Build: gcc -O2 -o burn burn.c
int main(void)
{
volatile unsigned long x = 0;
for (;;)
x++;
return 0;
}
Now build the cage and put the hog inside it (run as root):
# 1. Create a new cgroup - just make a directory in the v2 tree
$ sudo mkdir /sys/fs/cgroup/mygroup
# 2. Make sure the cpu controller is enabled for children of the root
$ cat /sys/fs/cgroup/cgroup.subtree_control
cpu memory pids # if 'cpu' is missing:
$ echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
# 3. Set the bandwidth limit: 25 ms of CPU per 100 ms period = 25%
$ echo "25000 100000" | sudo tee /sys/fs/cgroup/mygroup/cpu.max
# 4. Start the hog and move it into the cgroup
$ ./burn &
[1] 7412
$ echo 7412 | sudo tee /sys/fs/cgroup/mygroup/cgroup.procs
Open top in another terminal. The burn process, which was at 100% CPU, immediately drops to about 25% and stays there. The kernel enforces the ceiling every 100 ms period: once the group has consumed its 25 ms of quota, its runnable tasks are throttled until the next period begins.
Verify with cpu.stat
$ cat /sys/fs/cgroup/mygroup/cpu.stat
usage_usec 5241337
user_usec 5238012
system_usec 3325
nr_periods 210
nr_throttled 208
throttled_usec 15612044
nr_throttled climbing alongside nr_periods is the kernel telling you, plainly, that the limit is biting in almost every period. In production monitoring, a service with persistently high throttled_usec either needs a bigger quota or a diet.
Cleanup
$ kill 7412
$ sudo rmdir /sys/fs/cgroup/mygroup # must be empty of processes first
The Production Way: Let systemd Drive cgroups
Hand-editing sysfs files is perfect for learning, but on a systemd system, systemd owns the cgroup tree. The clean equivalents:
# One-off command capped at 25% of a CPU
$ sudo systemd-run --scope -p CPUQuota=25% ./burn
# Give an interactive shell a weight instead of a ceiling
$ sudo systemd-run --scope -p CPUWeight=50 bash
# Permanently limit a service in its unit file
# /etc/systemd/system/myapp.service
[Service]
ExecStart=/usr/local/bin/myapp
CPUQuota=150% # up to 1.5 CPUs on a multicore box
CPUWeight=200 # favoured 2:1 over default services under load
# Inspect what systemd wrote into the cgroup files
$ systemctl show myapp.service -p CPUQuotaPerSecUSec,CPUWeight
$ systemd-cgls # visualize the whole cgroup tree
$ systemd-cgtop # 'top' for cgroups
Under the hood, CPUQuota=25% is exactly our echo "25000 100000" > cpu.max, and CPUWeight= writes cpu.weight. Same kernel mechanism, cleaner lifecycle management.
What About cgroups v1?
You will still meet v1 in older books, legacy container hosts, and long-lived enterprise systems, so here is the translation table — learn it for reading old material, not for new work:
| Concept | cgroups v1 (legacy) | cgroups v2 (current) |
|---|---|---|
| Hierarchy | One tree per controller (/sys/fs/cgroup/cpu, .../memory, …) |
Single unified tree at /sys/fs/cgroup |
| CPU ceiling | cpu.cfs_quota_us + cpu.cfs_period_us |
cpu.max (both values in one file) |
| CPU proportion | cpu.shares (default 1024) |
cpu.weight (default 100) |
| Status in 2026 | Deprecated; removed from systemd (v258) and unavailable on RHEL 10 / Ubuntu 26.04 | The default and only supported mode on current distros |
Kernel configuration side note: cgroups is a compile-time kernel feature under General setup → Control Group support. On any distribution kernel it is already enabled; you can confirm with grep CGROUP /boot/config-$(uname -r). Only on fully custom embedded kernels would you ever need to switch it on yourself.
Real-World Use Cases
- Containers: every Docker/Podman/Kubernetes CPU limit you have ever set becomes a
cpu.maxwrite. Kubernetes now requires cgroups v2 on nodes. - Multi-tenant servers: per-user slices (
user-1000.slice) can be weighted or capped so no login session can monopolize the box. - Embedded systems: cap a telemetry or logging daemon at a few percent of the SoC so the real product function always has headroom — a very common pattern in embedded Linux projects.
- CI/build farms: give build jobs high weight but a hard ceiling, keeping the host responsive for monitoring agents.
Common Mistakes and Troubleshooting
| Problem | Explanation and Fix |
|---|---|
Writing cpu.max fails with “No such file” |
The cpu controller is not enabled for that subtree. Add it: echo "+cpu" > parent/cgroup.subtree_control. |
| Cannot add a process: “Device or resource busy” | v2’s no-internal-process rule: a cgroup with children cannot also hold processes. Put processes only in leaf cgroups. |
| Manual cgroup vanishes or fights with systemd | systemd considers itself the single writer of the tree. For anything persistent, use unit properties or systemd-run; use raw mkdir only for learning. |
| Latency spikes in a quota-limited service | Classic throttling stalls: the group burns its quota early in each period, then freezes. Raise the quota, shorten the period, or configure cpu.max.burst. |
Old tutorial paths like /sys/fs/cgroup/cpu/... do not exist |
That is the v1 layout. On a unified-hierarchy system everything lives directly under /sys/fs/cgroup — translate using the table above. |
Best Practices
- Prefer weights over quotas for services that merely need protection from each other — weights never waste idle CPU.
- Reserve quotas (
cpu.max) for true multi-tenancy, billing boundaries, or thermal/power capping on embedded hardware. - Monitor
cpu.statthrottling counters in production; persistent throttling is a capacity signal, not background noise. - Keep the hierarchy shallow and meaningful — deep nesting multiplies limit interactions and confuses debugging.
- On systemd machines, express everything as unit properties so limits survive reboots and service restarts.
Performance and Security Considerations
Performance: bandwidth enforcement itself is cheap, but aggressive quotas cause bursty latency: a service can be frozen for the tail of every period. For latency-sensitive workloads, use generous periods-to-quota ratios or burst budgets, and always verify with cpu.stat rather than assuming.
Security: cgroups v2 supports safe delegation — a subtree can be handed to an unprivileged user or container manager, who can then subdivide their own allocation but never exceed it. This containment property is precisely why real-time-capable and containerized workloads should always live inside a bounded cgroup, connecting back to the security note from our scheduling policy lesson.
Key Takeaways
- Scheduling priorities order threads; cgroups bound groups. You need both for a well-behaved system.
- cgroups v2 is the only interface that matters now: one unified tree at
/sys/fs/cgroup. cpu.maxis the hard ceiling (quota/period);cpu.weightis the proportional share under contention.cpu.stattells you honestly whether your limits are helping or hurting.- On systemd systems, drive cgroups through unit properties and
systemd-run, not raw sysfs writes.
Conclusion
CPU bandwidth control completes the scheduling picture we have built across the last three lessons: affinity pins work to CPUs, policy and priority order the work, and cgroups v2 bounds how much CPU any group of work may consume. These three mechanisms together are the foundation of everything from Kubernetes resource limits to keeping a background OTA update from stuttering the UI on an embedded device.
Try the lab on your own VM — watching top the moment you write to cgroup.procs and seeing a 100% hog collapse to 25% is one of those small demonstrations that makes the kernel feel wonderfully concrete. That hands-on habit is the heart of this free Linux kernel development course.
FAQ
What is CPU bandwidth control in Linux?
It is the ability, provided by the cgroups CPU controller, to cap or proportionally share the CPU time available to a group of processes — independent of thread counts or priorities.
How do I check whether my system uses cgroups v1 or v2?
Run mount | grep cgroup. A single cgroup2 mount on /sys/fs/cgroup means pure v2. Multiple cgroup (no “2”) mounts per controller indicate the legacy v1 layout.
What does “25000 100000” in cpu.max mean?
Quota and period in microseconds: the group may consume at most 25 ms of CPU time within every 100 ms window — effectively 25% of one CPU. max 100000 means unlimited.
What is the difference between cpu.weight and cpu.max?
cpu.weight divides CPU proportionally only when there is contention and never wastes idle cycles. cpu.max is an absolute ceiling enforced even on an idle machine.
Is cgroups v1 still usable in 2026?
Only on older or specialized systems. systemd removed v1 support, and current releases such as RHEL 10 and Ubuntu 26.04 ship v2-only. New learning and new deployments should be v2 exclusively.
Can I limit CPU for a single command without creating anything permanent?
Yes: systemd-run --scope -p CPUQuota=25% <command> creates a transient scope with the limit applied and cleans up automatically when the command exits.
Why is my process throttled even though the CPU is idle?
Because cpu.max is an absolute cap, not a fairness mechanism. If you only want protection under load, use cpu.weight instead.
Do cgroup CPU limits apply to real-time (SCHED_FIFO) threads?
Real-time tasks are handled by separate RT bandwidth accounting, and on non-RT kernels an RT task generally cannot even join a v2 cgroup with the cpu controller enabled unless RT group scheduling is configured. Treat RT workloads as a special case and test on your target kernel.
How do containers use this?
Container runtimes translate flags like --cpus=1.5 directly into cpu.max writes on the container’s cgroup. Understanding this lesson means you understand container CPU limits at the kernel level.
You Finished the CPU Scheduling Track!
Affinity, policy and priority, kernel threads, and now bandwidth control — all part of the free Linux kernel development course on EmbeddedPathashala.
