A Linux CPU cgroup does not make a process run faster or slower by itself. It puts one or more processes into a group and tells the kernel how much CPU time that whole group may consume. If the group reaches its quota, the scheduler throttles the group until the next scheduling period.
This is useful when one service, script, container, or user workload can consume too much CPU and affect the rest of the system. The important point is that the limit applies to the cgroup, not to each process individually. If a service has a parent process and several worker processes, all of them share the same CPU budget when they are in the same cgroup.
You do not need two processes to use cgroups. The examples use stress --cpu 2
because it creates visible CPU pressure and makes throttling easy to observe. In a real
setup, the workload might be one process, a web service with many workers, a systemd
service, or a container.
What this guide will do
This guide shows three practical ways to limit CPU on Linux:
| Method | Use it when | Main setting |
|---|---|---|
| systemd unit limit | You want to limit a service managed by systemd | CPUQuota= |
| systemd slice | Several services should share one CPU budget | CPUQuota= on *.slice, Slice= on services |
| direct cgroup v2 files | You are testing, learning, or building your own cgroup integration | cpu.max |
| cgroup v1 files | You maintain an older host that still uses legacy cgroups | cpu.cfs_quota_us |
For most modern Linux servers, use systemd CPUQuota= for services. It is persistent,
cleaner, and systemd manages the service cgroup for you. Use direct cgroup v2 commands
when you need to understand or test the underlying kernel interface.
This guide also separates two different actions that are often confused:
| Action | What it does |
|---|---|
| Create or configure a cgroup | Prepares a CPU limit, but does not automatically affect every process |
| Attach a process to that cgroup | Makes that process and its child processes use the cgroup limit |
CPU quota basics
A CPU quota is usually written as allowed CPU time per period.
For example, this cgroup v2 value means the group may use 50,000 microseconds of CPU runtime every 100,000 microseconds:
50000 100000That is 50% of one CPU. The percentage is based on one CPU, not on the whole machine.
If you are new to CPU sockets, cores, and threads, see
CPU, processors, cores, and threads explained.
To inspect CPU topology on a live host, you can also use the
Linux lscpu command.
| Limit | Meaning |
|---|---|
25% |
One quarter of one CPU |
50% |
Half of one CPU |
100% |
One full CPU |
200% |
Two full CPUs worth of CPU time |
400% |
Four full CPUs worth of CPU time |
On a server with more than one CPU core, the quota is still an aggregate CPU-time limit for the cgroup. For example:
| Host CPU count | Quota | What the cgroup may use |
|---|---|---|
| 1 CPU | 50% |
Half of the only CPU |
| 2 CPUs | 50% |
Half of one CPU, not half of both CPUs |
| 8 CPUs | 50% |
Half of one CPU, not 4 CPUs |
| 8 CPUs | 400% |
About 4 CPUs worth of total runtime |
The scheduler can run the cgroup's processes on different CPU cores over time unless you
also restrict CPU placement with cpuset controls such as AllowedCPUs= in systemd or
cpuset.cpus in cgroups. A quota limits how much CPU time the group gets; CPU
affinity or cpuset settings limit which CPUs it can run on.
A hard quota is different from a CPU weight:
| Setting type | What it does |
|---|---|
| Quota | Sets a hard cap. The group is throttled after it uses its budget. |
| Weight/share | Sets relative priority when CPUs are busy. It does not guarantee a fixed percentage cap. |
Prepare a test workload
Install stress so you can generate CPU load during the examples:
Debian / Ubuntu: sudo apt install stress
RHEL / Rocky / Alma: sudo dnf install stress
Alpine Linux: sudo apk add stressOn some distributions you can use stress-ng instead (often sudo apt install stress-ng
or sudo dnf install stress-ng). Replace stress --cpu 2 with
stress-ng --cpu 2 in the examples if you prefer that package.
The test command used below is:
$ stress --cpu 2 --timeout 60
This starts two CPU worker processes for 60 seconds. We use two workers only to prove that the cgroup limit is shared by all processes in the group. If you need to inspect worker threads or process counts on a real application, see how to check threads per process in Linux. If the group is capped at 50% of one CPU, both workers together should be limited to roughly that total budget.
Check whether the host uses cgroup v2 or v1
Most current distributions use unified cgroup v2. Check the cgroup filesystem type:
$ findmnt -no FSTYPE /sys/fs/cgroup
cgroup2
If the output is cgroup2, use the cgroup v2 and systemd examples below.
Also confirm that the CPU controller is available:
$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc dmem
The output must include cpu. The exact list of controllers can differ by kernel and
distribution.
How cgroup CPU limits become process specific
Writing cpu.max or cpu.cfs_quota_us does not limit the whole system. It only
sets a limit on that cgroup directory. The limit becomes process specific only when a
process is placed in that cgroup.
There are three common ways to do that:
| Workload type | Recommended way to place it in a CPU-limited cgroup |
|---|---|
| systemd service | Set CPUQuota= on the service. systemd places all service processes in the unit cgroup. |
| One-off command | Use systemd-run --scope -p CPUQuota=... or start the command after joining a manual cgroup. |
| Existing process | Write the PID to cgroup.procs in the target cgroup. Child processes created after that normally inherit the cgroup. Use commands from Linux process listing if you need to identify the PID first. |
For services, use systemd whenever possible. If you need a refresher on units, slices, and scopes, start with this systemd tutorial for Linux. systemd handles process tracking, restarts, child processes, and cleanup better than manual PID handling. Manual cgroup commands are useful for tests, learning, containers, and custom process managers.
Method 1: Limit a systemd service with CPUQuota=
Use this method for normal services such as web servers, application servers, queue workers, databases, and custom daemons. systemd already creates a cgroup for each service, so you only need to set the service property. If you are creating a service from scratch, see how to create a systemd service unit file.
Add a persistent CPU limit to a service
Open a systemd drop-in for the service:
$ sudo systemctl edit myapp.service
Add this content:
[Service]
CPUAccounting=yes
CPUQuota=50%Reload systemd and restart the service:
$ sudo systemctl daemon-reload
$ sudo systemctl restart myapp.service
This limits all processes in myapp.service to half of one CPU. If myapp.service
starts 1 process, that process gets the whole 50% budget. If it starts 10 worker processes,
all 10 workers share the same 50% budget.
You can also set the property without opening an editor:
$ sudo systemctl set-property myapp.service CPUAccounting=yes CPUQuota=50%
For a persistent service limit, prefer the drop-in file because it is easy to review and keep in configuration management.
Verify the systemd CPU limit
Replace myapp.service with your unit, then run these checks. For more service
discovery and status checks, see these systemctl service commands.
$ sudo systemctl show myapp.service -p CPUQuotaPerSecUSec
$ sudo systemctl show myapp.service -p ControlGroup --value
$ sudo cat /sys/fs/cgroup/system.slice/myapp.service/cpu.max
Until CPUQuota= is set, CPUQuotaPerSecUSec may read infinity and
cpu.max may show max 100000. After CPUQuota=50% is active, typical output
looks like:
CPUQuotaPerSecUSec=500ms
/system.slice/myapp.service
50000 100000With CPUQuota=25%, expect CPUQuotaPerSecUSec=250ms and 25000 100000.
Optional: use CPUWeight= for relative priority
Use CPUWeight= when you want relative priority, not a hard cap:
[Service]
CPUAccounting=yes
CPUWeight=50A service with a higher CPU weight gets more CPU time when the machine is busy. But if the
machine is idle, a weighted service can still use more CPU. For a strict maximum, use
CPUQuota=.
Optional: CPUQuotaPeriodSec=
The default quota period works for most services. You can tune how often the cap is
enforced with CPUQuotaPeriodSec= (for example 100ms) on the same unit. Shorter
periods can reduce long bursts before throttling; see systemd.resource-control for
details and trade-offs.
Multiple services sharing one CPU budget (slice)
To cap several units together, put them under a slice and set CPUQuota= (or
CPUWeight=) on the slice, then assign each service with Slice= in its unit.
# /etc/systemd/system/batch-work.slice.d/cpu.conf
[Slice]
CPUAccounting=yes
CPUQuota=150%# drop-in for a service that should live under that slice
[Service]
Slice=batch-work.sliceReload systemd and restart affected units. All processes in units under that slice share
the slice cgroup’s cpu.max.
Temporary systemd test with systemd-run
If you want to test CPU limiting without editing an existing service, run a temporary scope:
$ sudo systemd-run --unit=workload-cpu-cap.scope --scope -p CPUQuota=25% stress --cpu 2 --timeout 25
Running as unit: workload-cpu-cap.scope; invocation ID: beac8ee93d444967a97f7b850a55d8ed
The command and its child processes run inside the temporary scope cgroup. This is often the cleanest way to limit a one-off command without manually writing PIDs to cgroup files.
While the command is running, verify the limit:
$ sudo systemctl show workload-cpu-cap.scope -p CPUQuotaPerSecUSec -p ActiveState -p ControlGroup
ControlGroup=/system.slice/workload-cpu-cap.scope
CPUQuotaPerSecUSec=250ms
ActiveState=active
Read the cgroup files for that scope. usage_usec and the exact throttle counts change
every run; what matters is that nr_throttled and throttled_usec grow while the
load stays above the quota. For broader host-level validation, compare this with
Linux CPU utilization checks.
$ sudo cat /sys/fs/cgroup/system.slice/workload-cpu-cap.scope/cpu.max
25000 100000
$ sudo head -n 10 /sys/fs/cgroup/system.slice/workload-cpu-cap.scope/cpu.stat
usage_usec 875626
user_usec 576536
system_usec 299090
nice_usec 0
core_sched.force_idle_usec 0
nr_periods 35
nr_throttled 33
throttled_usec 4063716
nr_bursts 0
burst_usec 0
The important fields are:
| Field | Meaning |
|---|---|
nr_throttled |
Number of periods where the cgroup hit the CPU quota |
throttled_usec |
Total time the cgroup spent throttled |
Stop the temporary scope if it is still running:
$ sudo systemctl stop workload-cpu-cap.scope
Method 2: Limit CPU directly with cgroup v2 cpu.max
Use this method when you want to understand the kernel interface or test a temporary manual cgroup. For production services, prefer systemd unless you are writing your own cgroup manager.
Create a cgroup
Create a new cgroup directory for the workload:
$ sudo mkdir -p /sys/fs/cgroup/workload_cpu_cap
Set that cgroup to half of one CPU:
$ echo '50000 100000' | sudo tee /sys/fs/cgroup/workload_cpu_cap/cpu.max
50000 100000
At this point, no process is limited yet unless a process is already attached to
/sys/fs/cgroup/workload_cpu_cap. You have only configured the CPU budget for that
cgroup.
The two values are:
| Value | Meaning |
|---|---|
50000 |
CPU runtime allowed per period, in microseconds |
100000 |
Scheduler period, in microseconds |
If cpu.max is missing in the new directory, the CPU controller may not be enabled for
child cgroups. On a test machine, you can enable it on the parent:
$ echo +cpu | sudo tee /sys/fs/cgroup/cgroup.subtree_control
Then recreate the child cgroup and set cpu.max again. On a production host managed by
systemd, avoid changing the root cgroup layout casually; use systemd properties instead.
For non-root experiments, prefer cgroup delegation (see the kernel cgroup v2
documentation and your distribution’s notes) instead of altering the global root
cgroup.subtree_control.
Start a process inside the cgroup
A process joins a cgroup when its PID is written to cgroup.procs. Child processes then
inherit that cgroup.
This command starts a shell, moves that shell into the cgroup, and then replaces the shell
with stress:
$ bash -c 'echo $$ | sudo tee /sys/fs/cgroup/workload_cpu_cap/cgroup.procs >/dev/null; exec stress --cpu 2 --timeout 60' & PARENT=$!
This pattern avoids a common race where you start a parent process first and then try to find its worker PIDs afterward. Here, the parent and its workers start inside the cgroup from the beginning.
If you already have a running process, you can move that process into the cgroup by PID:
$ echo <PID> | sudo tee /sys/fs/cgroup/workload_cpu_cap/cgroup.procs
Only that process and processes later created inside the same cgroup are controlled by this limit. Other system processes remain unaffected.
Verify throttling
While stress is running, read cpu.stat. The absolute counters below are from an
example run; on your host, confirm nr_throttled and throttled_usec increase
under sustained load above the cap.
$ sudo head -n 10 /sys/fs/cgroup/workload_cpu_cap/cpu.stat
usage_usec 1625622
user_usec 985861
system_usec 639761
nice_usec 0
core_sched.force_idle_usec 0
nr_periods 35
nr_throttled 22
throttled_usec 2036643
nr_bursts 0
burst_usec 0
If nr_throttled and throttled_usec increase, the cgroup is hitting the quota.
That is the expected result when two CPU workers are placed under a 50% of one CPU cap.
You can also check which PIDs are in the cgroup:
$ sudo cat /sys/fs/cgroup/workload_cpu_cap/cgroup.procs
Stop the test and remove the cgroup
Stop the worker processes and the parent process. For more process termination options, see how to kill a process in Linux.
$ pkill -P $PARENT 2>/dev/null || true
$ kill $PARENT 2>/dev/null || true
Then remove the cgroup:
$ sudo rmdir /sys/fs/cgroup/workload_cpu_cap
The cgroup directory can be removed only after it is empty. If rmdir returns
Device or resource busy, one or more processes are still in that cgroup.
Useful cgroup v2 CPU files
| File | Meaning |
|---|---|
cpu.max |
Hard quota as quota period, or max for no quota |
cpu.stat |
Usage and throttling counters |
cpu.weight |
Relative CPU weight from 1 to 10000 |
cgroup.procs |
PIDs attached to the cgroup |
Method 3: Limit CPU on legacy cgroup v1 systems
Use this section only on older systems where the CPU controller is mounted as cgroup v1,
usually under /sys/fs/cgroup/cpu.
Check the filesystem type:
$ findmnt -no FSTYPE /sys/fs/cgroup/cpu
cgroup
Create a legacy CPU cgroup:
$ sudo mkdir -p /sys/fs/cgroup/cpu/legacy_workload_cpu_cap
Set half of one CPU using CFS quota files:
$ echo 100000 | sudo tee /sys/fs/cgroup/cpu/legacy_workload_cpu_cap/cpu.cfs_period_us
$ echo 50000 | sudo tee /sys/fs/cgroup/cpu/legacy_workload_cpu_cap/cpu.cfs_quota_us
These commands configure the legacy cgroup only. They do not limit all processes on the
system. A process must be attached to legacy_workload_cpu_cap before the quota affects
it.
These files map directly to the same quota idea:
| cgroup v1 file | Value for 50% of one CPU |
|---|---|
cpu.cfs_period_us |
100000 |
cpu.cfs_quota_us |
50000 |
Attach a process by writing its PID to cgroup.procs:
$ echo <PID> | sudo tee /sys/fs/cgroup/cpu/legacy_workload_cpu_cap/cgroup.procs
Some older examples use tasks instead of cgroup.procs. Prefer
cgroup.procs when it exists.
To remove the test cgroup, stop or move all attached processes first, then run:
$ sudo rmdir /sys/fs/cgroup/cpu/legacy_workload_cpu_cap
cgroup v1 and v2 comparison
| Topic | cgroup v1 | cgroup v2 |
|---|---|---|
| Layout | Separate controller mounts | One unified hierarchy |
| CPU quota file | cpu.cfs_quota_us |
cpu.max |
| CPU period file | cpu.cfs_period_us |
Second value in cpu.max |
| Process attachment | cgroup.procs or tasks |
cgroup.procs |
| Recommended for new systems | No | Yes |
What not to use for a hard CPU cap
Some Linux tools affect scheduling but do not replace CPU cgroup quotas:
niceandrenicechange process priority. They do not set a hard CPU percentage.sysctlchanges global kernel tunables under/proc/sys. It does not assign one service to a CPU budget. For global kernel tuning context, see these sysctl performance settings.sched_rt_runtime_uscontrols global real-time scheduler bandwidth. It is separate from normal CFS cgroup CPU quotas.
Use cgroups, systemd, or your container runtime when you need a per-service or per-workload CPU limit. If the workload is containerized, it also helps to understand Docker and containerd differences.
Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
cpu.max: No such file or directory |
CPU controller is not enabled for that cgroup subtree | Use systemd, or enable +cpu on the parent on a test host |
rmdir: Device or resource busy |
Processes are still inside the cgroup | Stop them or move them to another cgroup first |
nr_throttled stays at 0 |
The workload is not using more CPU than the quota, or it is not in the cgroup | Check cgroup.procs and use a CPU-heavy test |
| CPU usage looks higher than expected briefly | Monitoring tools sample over intervals and may show short bursts | Check cpu.max and cpu.stat for the authoritative cgroup state |
Frequently Asked Questions
1. What is the difference between cpu.max and cpu.weight in cgroup v2?
cpu.max sets a hard bandwidth cap for the whole cgroup. cpu.weight sets relative share when CPUs are busy; it is not a hard percentage cap by itself.2. Does CPUQuota=50% mean 50% of all CPUs on a multi-core server?
No. CPUQuota=50% means half of one CPU. On any server, CPUQuota=200% means about two CPUs worth of aggregate CPU time for the whole service or cgroup.3. Do I need two processes to use cgroups?
No. A cgroup can contain one process or many processes. The examples use stress --cpu 2 only to create enough CPU load to make throttling easy to observe.4. Does writing cpu.max limit all processes on the system?
No. cpu.max only limits processes attached to that specific cgroup. You attach processes by writing PIDs to cgroup.procs, or by using systemd so it places service processes in the unit cgroup.5. What is the best way to limit CPU for a Linux service?
For a systemd-managed service, use systemd CPUQuota= in a service drop-in or systemctl set-property. systemd manages the service cgroup and applies the underlying cgroup CPU limit.6. Can sysctl replace cgroups for limiting one service?
No. sysctl changes global kernel parameters under /proc/sys. Per-process or per-service CPU budgets use cgroups, systemd, or a container runtime.Summary
For a systemd service, set CPUQuota= and let systemd manage the cgroup. For learning or
low-level testing on cgroup v2, create a cgroup, set cpu.max, and start or move the
workload into that cgroup. For older cgroup v1 systems, use cpu.cfs_period_us and
cpu.cfs_quota_us, then attach the target PID to that cgroup.
The key rule is simple: a CPU limit belongs to a group of processes. Whether that group
contains one process or many workers, the total CPU usage of the group is capped by the
quota. The quota is also based on one CPU: 50% means half of one CPU, while
200% means about two CPUs worth of CPU time.

