How to limit CPU using cgroups (v2 and v1) on Linux

Tech reviewed: Deepak Prasad
How to limit CPU using cgroups (v2 and v1) on Linux

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:

text
50000 100000

That 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:

text
Debian / Ubuntu:       sudo apt install stress
RHEL / Rocky / Alma:   sudo dnf install stress
Alpine Linux:          sudo apk add stress

On 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:

console
$ 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:

console
$ 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:

console
$ 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:

console
$ sudo systemctl edit myapp.service

Add this content:

ini
[Service]
CPUAccounting=yes
CPUQuota=50%

Reload systemd and restart the service:

console
$ 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:

console
$ 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.

console
$ 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:

text
CPUQuotaPerSecUSec=500ms
/system.slice/myapp.service
50000 100000

With 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:

ini
[Service]
CPUAccounting=yes
CPUWeight=50

A 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.

ini
# /etc/systemd/system/batch-work.slice.d/cpu.conf
[Slice]
CPUAccounting=yes
CPUQuota=150%
ini
# drop-in for a service that should live under that slice
[Service]
Slice=batch-work.slice

Reload 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:

console
$ 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:

console
$ 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.

console
$ 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:

console
$ 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:

console
$ sudo mkdir -p /sys/fs/cgroup/workload_cpu_cap

Set that cgroup to half of one CPU:

console
$ 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:

console
$ 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:

console
$ 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:

console
$ 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.

console
$ 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:

console
$ 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.

console
$ pkill -P $PARENT 2>/dev/null || true
$ kill $PARENT 2>/dev/null || true

Then remove the cgroup:

console
$ 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:

console
$ findmnt -no FSTYPE /sys/fs/cgroup/cpu
cgroup

Create a legacy CPU cgroup:

console
$ sudo mkdir -p /sys/fs/cgroup/cpu/legacy_workload_cpu_cap

Set half of one CPU using CFS quota files:

console
$ 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:

console
$ 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:

console
$ 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:

  • nice and renice change process priority. They do not set a hard CPU percentage.
  • sysctl changes 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_us controls 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.


References

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with over a decade of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, …

  • Red Hat Certified System Administrator in Red Hat OpenStack
  • Certified Kubernetes Application Developer (CKAD)
  • Red Hat Certified Specialist in Ansible Automation
  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security