How Does cpu.max Control CPU Usage in Linux?

Linux Cgroups v2 CPU Controller: Limit CPU Usage of Any Process

Part of our free Linux kernel development course — learn how the kernel divides CPU time between groups of processes using cgroups v2, with hands-on terminal examples you can try today.

Level
Intermediate
Reading Time
~15 minutes
Kernel Version
6.12 LTS – 7.1

Have you ever wondered how Docker limits a container to half a CPU core? Or how systemd stops one runaway service from starving the rest of your system? The answer is the Linux cgroups v2 CPU controller, a kernel feature that lets you decide exactly how much CPU time a group of processes is allowed to consume.

In this lesson from our free Linux kernel development course, you will build a CPU-limited group by hand using nothing but a shell and the files under /sys/fs/cgroup. Once you see it work with your own eyes, container CPU limits will never feel like magic again. Everything here is tested against modern kernels (6.12 LTS through the current 7.x series), where cgroups v2 is the default on every major distribution.

What You Will Learn

What cgroups are and why the kernel needs them
Difference between cgroups v1 and cgroups v2
The unified hierarchy under /sys/fs/cgroup
How cpu.max quota and period work
Hands-on: throttle a real process to 10% CPU
cpu.weight for proportional sharing
How containers use the CPU controller
Common mistakes and troubleshooting

Prerequisites

Basic Linux command line
What a process and PID are
Root or sudo access on a test machine
Any distro with kernel 5.15 or newer

You do not need to write any C code for this lesson. The entire cgroups v2 CPU controller interface is exposed as plain files, so a shell is enough. If you are following our free Linux device drivers course as well, the concepts here (kernel-managed hierarchies exposed through a virtual filesystem) will feel very familiar from sysfs.

What Are Control Groups (cgroups)?

A control group is a kernel mechanism for putting processes into named groups and then applying resource rules to the whole group at once. Instead of saying “process 4312 may use limited CPU”, you say “everything inside the group called build_jobs may together use at most 50% of one CPU”. Children of those processes automatically inherit the rule, which is exactly what you want for services and containers that fork constantly.

Each resource type is managed by a controller. The kernel ships controllers for CPU time, memory, block I/O, PIDs, and more. In this lesson we focus on the CPU controller, because CPU is the resource where unfair sharing is most visible: one busy loop can make an entire machine feel sluggish.

Cgroups v2 Unified Hierarchy (simplified)
/sys/fs/cgroup  (root cgroup)
▼   ▼   ▼
system.slice
(system services)
user.slice
(login sessions)
test_group
(our demo group)
▼
cpu.max = “100000 1000000”
job1 (PID)  •  job2 (PID)

Why cgroups v2 Replaced v1

The original cgroups v1 design allowed a separate hierarchy per controller. That sounded flexible but turned into a mess: a process could sit in one place in the CPU tree and a completely different place in the memory tree, and controllers could not cooperate. Cgroups v2 fixes this with a single unified hierarchy: one tree, all controllers, consistent rules. On any recent distribution (Ubuntu 22.04+, Debian 12+, Fedora, RHEL 9+, Arch), v2 is mounted by default and v1 is effectively legacy.

Cgroups v1 vs Cgroups v2 at a Glance
Aspect cgroups v1 (legacy) cgroups v2 (current)
Hierarchy Multiple trees, one per controller Single unified tree
CPU limit files cpu.cfs_quota_us + cpu.cfs_period_us One file: cpu.max
CPU shares cpu.shares (default 1024) cpu.weight (1–10000, default 100)
Processes location Any node in the tree Leaf nodes only (no-internal-process rule)
Default on modern distros No Yes (systemd unified mode)

How the Cgroups v2 CPU Controller Enforces Limits

The Linux cgroups v2 CPU controller offers two independent knobs, and understanding the difference is the single most important idea in this lesson:

  • cpu.max (bandwidth limit, a hard ceiling): “This group may run for at most QUOTA microseconds inside every PERIOD microseconds.” Even if the CPU is otherwise idle, the group is throttled once its quota is spent.
  • cpu.weight (proportional share, a soft ratio): “When groups compete, divide CPU time according to their weights.” If nobody is competing, a group can use the whole CPU.

The bandwidth mechanism works in repeating windows. The default period is 100,000 microseconds (100 ms). If you write a quota of 10,000 with that period, your group gets 10 ms of CPU per 100 ms window — a 10% CPU cap. When the quota runs out mid-window, the scheduler simply refuses to run the group’s threads until the next window starts. That forced pause is called throttling, and you can observe it directly in cpu.stat.

cpu.max in Action: quota 20000, period 100000 (20% cap)
RUN
0–20 ms
THROTTLED (waits) 20–100 ms
RUN
100–120 ms
THROTTLED (waits) 120–200 ms
Each row is one 100 ms period. Green = group runs, grey = group is throttled by the CPU controller.

A Note on the Modern Scheduler (EEVDF)

Older books describe the CPU controller on top of CFS, the Completely Fair Scheduler. Since kernel 6.6, CFS has been replaced by EEVDF (Earliest Eligible Virtual Deadline First). The good news for you: the cgroup interface did not change. cpu.max, cpu.weight, and cpu.stat behave the same; only the underlying fair-scheduling algorithm that honors them is newer and better at latency. So everything you practice below applies unchanged to kernel 6.6, 6.12 LTS, 6.18 LTS, and the 7.x series.

Hands-On: Throttle a Process with the Cgroups v2 CPU Controller

Time to prove it works. We will create a group, give it a tiny CPU budget, drop two busy jobs into it, and watch how little work they manage to do. Run these on a test machine as root.

Step 1 — Confirm cgroups v2 Is Mounted

$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc

If cpu appears in cgroup.controllers, the kernel supports the CPU controller. Seeing cgroup2 as the filesystem type confirms the unified hierarchy.

Step 2 — Enable the CPU Controller for Child Groups

# echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control
# cat /sys/fs/cgroup/cgroup.subtree_control
cpu memory pids

This is a v2 concept many beginners miss: a controller must be delegated downward from a parent before child groups can use it. On systemd machines, cpu is usually already enabled here.

Step 3 — Create Our Group and Set a 10% Limit

# mkdir /sys/fs/cgroup/demo_group
# echo "10000 100000" > /sys/fs/cgroup/demo_group/cpu.max
# cat /sys/fs/cgroup/demo_group/cpu.max
10000 100000

Read that as: quota 10,000 µs per period of 100,000 µs — a 10% ceiling. The word max in place of the quota means “unlimited”, which is the default.

Step 4 — Launch Two Counting Jobs and Put Them in the Group

We will use a small original counter script. Each job prints an incrementing number as fast as it can, starting from a base value you pass in, so we can tell the two jobs apart in the output files:

#!/bin/bash
# counter.sh -- print numbers starting from $1, forever
i=${1:-1}
while true; do
    echo -n "$i "
    i=$((i + 1))
done
# ./counter.sh 1    > /tmp/job_a.txt &
# ./counter.sh 5000 > /tmp/job_b.txt &
# echo $! > /dev/null   # note the PIDs; suppose they are 4101 and 4102

# echo 4101 > /sys/fs/cgroup/demo_group/cgroup.procs
# echo 4102 > /sys/fs/cgroup/demo_group/cgroup.procs

# cat /proc/4101/cgroup
0::/demo_group

Writing a PID into cgroup.procs migrates the whole process into the group instantly. The /proc/PID/cgroup check confirms membership.

Step 5 — Let Them Run, Then Compare

# sleep 5
# kill 4101 4102
# wc -w /tmp/job_a.txt /tmp/job_b.txt
   118 /tmp/job_a.txt
   121 /tmp/job_b.txt

Roughly a hundred numbers each in five seconds. Now repeat the experiment after raising the limit to 80%:

# echo "80000 100000" > /sys/fs/cgroup/demo_group/cpu.max
# (relaunch both jobs, add PIDs to the group, sleep 5, kill)
# wc -w /tmp/job_a.txt /tmp/job_b.txt
  9834 /tmp/job_a.txt
  9781 /tmp/job_b.txt

The exact numbers depend on your CPU, but the ratio is the story: raising the CPU bandwidth from 10% to 80% multiplied the completed work dramatically. That is the cgroups v2 CPU controller doing its job. Also notice the two jobs did nearly equal work — within a group, the scheduler still shares fairly between members.

Step 6 — Inspect Throttling Statistics and Clean Up

# cat /sys/fs/cgroup/demo_group/cpu.stat
usage_usec 501873
user_usec 498211
system_usec 3662
nr_periods 50
nr_throttled 49
throttled_usec 4451209

# rmdir /sys/fs/cgroup/demo_group

nr_throttled counting 49 of 50 periods tells you the group hit its quota in almost every window. One practical warning: rmdir fails with “Device or resource busy” if any process is still inside the group, so kill or migrate members first.

cpu.weight: Proportional Sharing Without Hard Caps

Hard caps waste idle CPU. If your only goal is “the build farm should not starve the web server”, use weights instead:

# mkdir /sys/fs/cgroup/web /sys/fs/cgroup/builds
# echo 300 > /sys/fs/cgroup/web/cpu.weight
# echo 100 > /sys/fs/cgroup/builds/cpu.weight

Under contention, the web group receives roughly three times the CPU of the builds group (300:100). When the web server is idle, the builds group happily consumes everything. Weights range from 1 to 10000 with a default of 100, and only ratios between siblings matter, not absolute values.

Real-World Uses: Containers, systemd, and Kubernetes

Everything you just did by hand is what the container ecosystem automates:

  • Docker / Podman: docker run --cpus=1.5 simply writes 150000 100000 into the container’s cpu.max.
  • Kubernetes: a pod’s CPU limit becomes cpu.max, while its CPU request maps to cpu.weight. Two knobs, one controller.
  • systemd: CPUQuota=25% in a unit file, or on the fly: systemctl set-property nginx.service CPUQuota=25%. On systemd machines you should manage long-lived limits through systemd rather than raw mkdir, so the two do not fight over the tree.
  • Embedded Linux: on a multi-core automotive or industrial board, cgroups keep a misbehaving telemetry daemon from stealing cycles needed by control loops — often combined with cpuset pinning.

Common Mistakes and Troubleshooting

1. “No such file or directory” when writing cpu.max. The cpu controller was not enabled in the parent’s cgroup.subtree_control. Enable it one level up first.

2. “Device or resource busy” on rmdir. Processes (or child groups) still live inside. Check cgroup.procs, migrate or kill them, then remove.

3. Writing a PID to a non-leaf group fails. Cgroups v2 enforces the no-internal-process rule: once a group has children with controllers enabled, processes may only sit in the leaves.

4. Limit seems ignored. You may have written to a v1 mount on an older system, or the process forked and the child escaped before you migrated the parent. Writing to cgroup.procs moves a process and its future children, not children forked earlier.

5. Quota below 1000 rejected. The kernel enforces a minimum quota of 1 ms (1000 µs); asking for less returns an error.

Best Practices and Performance Considerations

  • Prefer cpu.weight for fairness and reserve cpu.max for cases needing a genuine ceiling (billing, thermal limits, noisy-neighbor isolation). Hard caps leave CPU idle on purpose.
  • Keep the default 100 ms period unless you have a latency reason to shrink it. Smaller periods give smoother throttling but add scheduler overhead; larger periods cause longer visible stalls when the quota runs out.
  • Watch nr_throttled and throttled_usec in production. Heavy throttling on a latency-sensitive service is a classic cause of mysterious tail-latency spikes, especially for multi-threaded programs that burn the whole quota in the first few milliseconds of each period.
  • On systemd systems, express policy in unit files so it survives reboots and stays visible to the rest of the system.
  • Security angle: delegation lets you hand a subtree to an unprivileged user (this is how rootless containers work). Only delegate the subtree you intend to, and remember cgroup limits are resource controls, not a sandbox by themselves — combine them with namespaces.

Key Takeaways

  • Cgroups group processes into a tree and apply resource rules per group; the CPU controller governs CPU time.
  • Cgroups v2 uses one unified hierarchy under /sys/fs/cgroup and is the default everywhere today.
  • cpu.max = “QUOTA PERIOD” is a hard bandwidth ceiling; cpu.weight is a soft proportional share.
  • Everything is a file: mkdir creates a group, echo sets limits, cgroup.procs moves processes, cpu.stat shows throttling.
  • The scheduler changed from CFS to EEVDF in kernel 6.6, but the cgroup interface stayed identical.
  • Docker, Kubernetes, and systemd CPU limits are thin wrappers over exactly these files.

Conclusion

The Linux cgroups v2 CPU controller turns “please behave” into an enforceable contract: a group of processes gets the CPU budget you assign, verified in windows of a few milliseconds by the kernel scheduler itself. In this lesson you built a group from raw files, throttled real jobs to 10% and then 80%, read the throttling counters, and connected the mechanism to the tools you use daily — containers, Kubernetes, and systemd.

This lesson belongs to our free Linux kernel development course here on EmbeddedPathashala, where we also run a free Linux device drivers course and a free embedded systems course. In the next lecture we take the other side of the scheduling story: instead of limiting CPU, we make the kernel guarantee CPU on time, by turning Linux into a real-time operating system with PREEMPT_RT.

Frequently Asked Questions (FAQ)

1. What is the cgroups v2 CPU controller in Linux?

It is a kernel controller that limits and distributes CPU time among groups of processes. You set a hard bandwidth cap through the cpu.max file or a proportional share through cpu.weight, and the scheduler enforces it automatically for every process in the group.

2. How do I check whether my system uses cgroups v1 or v2?

Run mount | grep cgroup. If you see a single cgroup2 mount on /sys/fs/cgroup, you are on v2. A long list of separate v1 mounts (cpu, memory, blkio, …) means legacy or hybrid mode. You can also run stat -fc %T /sys/fs/cgroup; the answer cgroup2fs means v2.

3. What does “10000 100000” in cpu.max mean?

Quota then period, both in microseconds: the group may run 10,000 µs in every 100,000 µs window, i.e. a 10% CPU cap. Writing max 100000 removes the cap. A quota larger than the period (for example 200000 100000) allows two full CPUs’ worth of time on multi-core machines.

4. What is the difference between cpu.max and cpu.weight?

cpu.max is an absolute ceiling that applies even when the CPU is idle. cpu.weight only matters under contention: it decides the ratio in which competing sibling groups share CPU, and lets any group use idle capacity freely.

5. Can I use the cgroups v2 CPU controller without containers?

Yes. Containers are just one consumer. You can mkdir a group, write limits, and move any ordinary PID into it, or use systemd-run --property=CPUQuota=20% ./mytask for a one-shot limited command.

6. Why does removing my cgroup directory fail?

rmdir only succeeds on an empty group. If any process is still listed in cgroup.procs, or a child group still exists, the kernel returns “Device or resource busy”. Migrate or terminate the members first.

7. Does the switch from CFS to EEVDF change cgroup behavior?

No interface changes. EEVDF (kernel 6.6+) replaced CFS as the fair scheduling algorithm, but cpu.max, cpu.weight, and cpu.stat work exactly as before. You may notice improved latency fairness, which is a bonus, not a migration task.

8. Is this covered in a free Linux kernel development course?

Yes — this article is one lecture of the free Linux kernel development course on EmbeddedPathashala. The course also connects into our free Linux device drivers course and free embedded systems course, so you can move from user-space resource control down to kernel modules and driver code at no cost.

Continue Your Free Kernel Programming Journey

Master the Linux scheduler, cgroups, kernel modules, and device drivers — completely free, in simple language, with hands-on labs.

Browse All Free Courses
Free Linux Device Drivers Course

Leave a Reply

Your email address will not be published. Required fields are marked *