>_ josetilabs.com
← blog

22 Oct 2026 · linux, cgroups, containers, kernel · 4 min

What cgroups are: how linux limits cpu and memory

A container is not a virtual machine. It is an ordinary Linux kernel process that receives a resource limit via cgroup. The cgroup is the structure that says: this process may use at most 256 MB of memory and half a CPU. Without a cgroup, the process competes with everything else on the host.

The answer paragraph

cgroup (control group) is the kernel structure that groups processes and applies limits on CPU, memory, I/O and number of processes. When you run docker run -m 256m --cpus 0.5, Docker creates a cgroup with memory.max = 268435456 bytes and cpu.max = 50000 100000. The process inside the container sees these numbers in /sys/fs/cgroup.

What the kernel shows

Terminal with two panels: without a limit memory.max shows as max and cpu.max as max 100000; with -m 256m and --cpus 0.5 they show 268435456 bytes and quota 50000 us per 100000 us
The same script in both cases. The limit becomes a file in /sys/fs/cgroup.

Without a limit, memory.max is max (no ceiling) and cpu.max is max 100000 (no quota). With -m 256m --cpus 0.5, the kernel writes 268435456 and 50000 100000. The CPU quota means: in every period of 100000 microseconds, the group may consume at most 50000 microseconds of CPU. That is half a processor.

The artifact: reads the cgroup of its own process

python
#!/usr/bin/env python3
# cgroup-read.py
# Reads the cgroup v2 files of its own process and shows what the kernel sees.
# Run inside a container with a limit to see the numbers change:
#   docker run --rm -m 256m --cpus 0.5 python:3.12-slim python3 cgroup-read.py
import pathlib

base = pathlib.Path('/sys/fs/cgroup')

def read(name):
    p = base / name
    if not p.exists():
        return None
    try:
        return p.read_text().strip()
    except OSError:
        return None

def human(n):
    n = int(n)
    for unit in ('B', 'KiB', 'MiB', 'GiB'):
        if n < 1024 or unit == 'GiB':
            return f'{n} {unit}'
        n /= 1024
    return f'{n} GiB'

mem_max = read('memory.max')
mem_cur = read('memory.current')
cpu_max = read('cpu.max')
cpu_w = read('cpu.weight')
pids_max = read('pids.max')

print('cgroup v2 of the current process')
print('===========================')
if mem_max is not None:
    if mem_max == 'max':
        print('memory.max    : no limit (max)')
    else:
        print(f'memory.max    : {mem_max} bytes ({human(mem_max)})')
if mem_cur is not None:
    print(f'memory.current: {mem_cur} bytes ({human(mem_cur)})')
    if mem_max and mem_max != 'max':
        pct = int(mem_cur) * 100 // int(mem_max)
        print(f'              : {pct}% of the limit in use')
if cpu_max is not None:
    parts = cpu_max.split()
    if parts and parts[0] == 'max':
        print('cpu.max       : no limit (max)')
    else:
        quota, period = parts[0], parts[1]
        cpus = int(quota) / int(period)
        print(f'cpu.max       : quota {quota} us / period {period} us = {cpus:.2f} CPU')
if cpu_w is not None:
    print(f'cpu.weight    : {cpu_w} (default 100; higher = more priority under contention)')
if pids_max is not None:
    print(f'pids.max      : {pids_max}')

# cgroup v1 (fallback on older hosts)
v1_mem = pathlib.Path('/sys/fs/cgroup/memory/memory.limit_in_bytes')
if v1_mem.exists() and mem_max is None:
    print('memory.limit_in_bytes (v1):', v1_mem.read_text().strip())
v1_cpu = pathlib.Path('/sys/fs/cgroup/cpu/cpu.cfs_quota_us')
if v1_cpu.exists() and cpu_max is None:
    print('cpu.cfs_quota_us (v1):', v1_cpu.read_text().strip())

The script installs nothing and runs in any container with Python 3.

How to verify

Run the script without a limit and with a limit, and check the difference. Expected output, copied from a real run:

Without a limit:

memory.max    : no limit (max)
cpu.max       : no limit (max)
cpu.weight    : 100

With -m 256m --cpus 0.5:

memory.max    : 268435456 bytes (256.0 MiB)
cpu.max       : quota 50000 us / period 100000 us = 0.50 CPU
cpu.weight    : 100

If memory.max shows max even with -m, the container is running on cgroup v1 or the runtime did not apply the limit. Check the Docker and kernel versions.

If it goes wrong

memory.max shows max even with -m 256m. The runtime may not have applied the limit, or the host uses cgroup v1 and the path is different. The script tries the v1 fallback (memory.limit_in_bytes). If it still does not show up, run docker inspect on the container and check HostConfig.Memory.

cpu.max shows max 100000 even with –cpus 0.5. Same problem: limit not applied. On cgroup v1, the files are cpu.cfs_quota_us and cpu.cfs_period_us. The script covers both.

The script fails with FileNotFoundError. The path /sys/fs/cgroup may not be mounted in the container (rare, but happens in restricted environments). Mount with --cgroupns host or run on the host directly.

The memory limit kills the process (OOM killed). That is expected: when memory.current reaches memory.max, the kernel invokes the OOM killer. The post Docker memory limits covers how to avoid it.

cgroup v1 and v2

Version Memory file CPU file When it appears
v2 (unified) memory.max cpu.max (quota period) kernel >= 5.0, recent hosts
v1 (hierarchy) memory/memory.limit_in_bytes cpu/cpu.cfs_quota_us older hosts, some managed environments

Most current hosts use v2. The script detects which is present and uses the v1 fallback when needed.

FAQ

Is a container just a cgroup? No. A container is namespaces (isolation of view: processes, network, mounts) plus cgroup (resource limit) plus a file system image. The cgroup is the part that answers the question ‘how much can I use’.

What happens when the process exceeds the memory limit? The kernel kills the process (OOM kill) or returns an allocation error, depending on configuration. There is no swap by default unless you configure it.

What is cpu.weight for? When two groups contend for CPU and both are at the limit, the weight decides the proportion. Default 100. A group with weight 200 gets twice as much under contention.

How do I see the cgroup of a process on the host? Read /proc/<pid>/cgroup. It points to the cgroup path in /sys/fs/cgroup. From there, read the files.

Do Docker and Kubernetes use cgroup differently? Kubernetes passes the pod limits to the runtime (containerd or CRI-O), which creates the cgroup. The mechanism is the same; the difference is who writes the files.

Sources: cgroups(7), kernel docs cgroup v2, Docker resource constraints. See also Docker memory limits and container without root.

Read next

// By e-mail

Posts published here also go out by e-mail. Delivery has not started yet: whoever leaves an address now joins the first batch.

I store only your e-mail, the date and the record of this consent. No cookies, no trackers, nothing shared with third parties. Details in the privacy policy.