| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64 |
| Applies to | Linux hosts with Podman where you need to cap container or pod CPU, memory, process count, or block I/O |
| Privilege | Rootful examples on the lab host; rootless cgroup v2 notes where delegation differs |
| Scope | --memory, --memory-reservation, --memory-swap, --cpus, --cpu-shares, --cpuset-cpus, --cpuset-mems (single-NUMA note), --pids-limit, --blkio-weight, per-device I/O flags at a high level, pod-level limits, brief Quadlet pointer, cgroup v2 verification through inspect and /sys/fs/cgroup, rootless systemd delegation and controller errors, and rootless cgroup v1 limitations. Does not cover podman stats monitoring workflows, kernel-wide tuning, Prometheus or Grafana, or Kubernetes resource limits. |
| Related guides | Inspect Podman containers Podman architecture |
Resource limits turn “this container may consume the whole host” into a bounded cgroup. Podman maps familiar flags such as --memory and --cpus into cgroup v2 files on modern Linux. The practical skill is not only passing flags — it is proving the kernel received the limit you intended.
Every section below follows the same pattern: configure, inspect, and read cgroup files. After create time, podman update can raise or lower CPU and memory ceilings on the same container without recreating it. For ongoing utilization after limits are set, use Monitor containers with podman stats.
Check cgroup version and manager first
Confirm the host before relying on rootless limits or specific controllers:
podman info --format '{{.Host.CgroupsVersion}} {{.Host.CgroupManager}}'Sample output on RHEL 10.2:
v2 systemdRootless resource limits work best with cgroup v2 and a systemd user manager that delegates controllers. Many flags are unsupported or unreliable on rootless cgroup v1 — treat that combination as legacy guidance only.
Lab pattern: discover the cgroup path
Most verification steps reuse one discovery chain. Read the container init PID:
podman inspect --format '{{.State.Pid}}' CONTAINERFollow /proc/PID/cgroup to the path under /sys/fs/cgroup/. The examples below use paths discovered from real containers — your IDs will differ, but the slice naming pattern stays the same on rootful systemd hosts.
Set a memory limit with --memory
Cap resident memory for a long-running container:
podman run -d --name mem-limit-demo --memory 512m docker.io/library/alpine:latest sleep 3600Confirm Podman stored the byte value:
podman inspect --format '{{.HostConfig.Memory}}' mem-limit-demoSample output:
536870912536870912 bytes is 512 MiB. Turn the cgroup line from /proc into a path under /sys/fs/cgroup and read the hard limit:
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' mem-limit-demo)/cgroup)/memory.max"Sample output:
536870912memory.max matching inspect is the proof the limit landed in the kernel.
Memory reservation vs hard limit
--memory sets the hard cap. --memory-reservation sets a softer floor Podman maps into cgroup v2 memory.low on this host:
podman run -d --name mem-reserve --memory 512m --memory-reservation 256m docker.io/library/alpine:latest sleep 3600Inspect both values:
podman inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}}' mem-reserveSample output:
536870912 268435456On the cgroup path, memory.low matched the reservation bytes:
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' mem-reserve)/cgroup)/memory.low"Sample output:
268435456memory.max on the same cgroup stayed 536870912. Reservation influences reclaim pressure; the hard limit still comes from --memory.
Configure swap with --memory-swap
--memory-swap is the combined memory-plus-swap ceiling, not “swap only”:
podman run -d --name mem-swap --memory 512m --memory-swap 1g docker.io/library/alpine:latest sleep 3600Inspect totals:
podman inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemorySwap}}' mem-swapSample output:
536870912 10737418241073741824 bytes is 1 GiB total. With 512 MiB RAM, roughly 512 MiB of additional swap is available when the host provides swap and cgroup swap accounting is enabled. Confirm swap headroom in the cgroup:
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' mem-swap)/cgroup)/memory.swap.max"Sample output:
536870912That value is swap space above RAM — 512 MiB in this example. -1 for unlimited swap behavior is supported on some configurations; prefer explicit totals in production manifests so limits stay auditable.
Set a CPU quota with --cpus
The simplest hard CPU cap for most users:
podman run -d --name cpu-limit --cpus 0.5 docker.io/library/alpine:latest sh -c 'while true; do :; done'Inspect the nanosecond representation:
podman inspect --format '{{.HostConfig.NanoCpus}}' cpu-limitSample output:
500000000Read cgroup v2 quota and period from the same discovery pattern:
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' cpu-limit)/cgroup)/cpu.max"Sample output on the lab container:
50000 10000050000 of 100000 microseconds per period is half of one CPU — matching --cpus 0.5. Under load, podman stats reported about 50% CPU for this container.
--cpu-shares is not a hard CPU limit
Shares only matter when containers compete. Start two busy loops with different weights:
podman run -d --name cpu-share-high --cpu-shares 1024 docker.io/library/alpine:latest sh -c 'while true; do :; done'Give the second container half the weight:
podman run -d --name cpu-share-low --cpu-shares 512 docker.io/library/alpine:latest sh -c 'while true; do :; done'After both containers warm up, compare utilization:
podman stats --no-stream --format '{{.Name}} {{.CPU}}' cpu-share-high cpu-share-lowSample output on a three-core host:
cpu-share-high 94.26
cpu-share-low 65.43Under contention the higher share received more CPU. On cgroup v2 the weights mapped to cpu.weight values 100 and 59. When no competitor exists, a low-share container can still use spare CPU — shares are relative scheduling weights, not caps. Use --cpus when you need a quota-like limit.
Pin CPUs with --cpuset-cpus
CPU sets restrict which processors a container may use — they do not assign a percentage of CPU time:
podman run -d --name cpuset-demo --cpuset-cpus 0 docker.io/library/alpine:latest sh -c 'while true; do :; done'Verify effective CPUs in the cgroup:
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' cpuset-demo)/cgroup)/cpuset.cpus.effective"Sample output:
0Inside the container namespace, the allowed list showed CPU 0 only. --cpuset-cpus answers “which cores,” not “how much time on those cores.”
NUMA memory nodes with --cpuset-mems
On hosts with multiple NUMA nodes, --cpuset-mems pins memory allocation to specific nodes. This lab VM reports one node:
lscpu | grep 'NUMA node(s)'Sample output:
NUMA node(s): 1With a single node, skip live --cpuset-mems demos — the flag matters on multi-socket hardware where memory locality affects performance.
Limit process count with --pids-limit
Cap forks with the cgroup PID controller, not shell ulimit:
podman run -d --name pids-demo --pids-limit 20 docker.io/library/alpine:latest sleep 3600Confirm the cgroup ceiling:
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' pids-demo)/cgroup)/pids.max"Sample output:
20Try to exceed the cap:
podman exec pids-demo sh -c 'for i in $(seq 1 25); do sleep 3600 & done'Sample output:
sh: fork: retry: Resource temporarily unavailable
sh: fork: Resource temporarily unavailableThe container stayed running — forks failed once the cgroup PID budget was exhausted. ulimit -u inside the shell is a different mechanism; use --pids-limit for container-wide enforcement.
Block I/O weight with --blkio-weight
I/O weight is relative, like CPU shares — it does not guarantee throughput:
podman run -d --name blkio-demo --blkio-weight 500 docker.io/library/alpine:latest sleep 3600On cgroup v2 this host exposes BFQ weight instead of legacy blkio.weight:
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' blkio-demo)/cgroup)/io.bfq.weight"Sample output:
default 500The flag mapped to io.bfq.weight. Weighting only matters when workloads contend for the same block device.
Per-device I/O limits
Podman also accepts device-level throttles when the runtime and cgroup controllers support them:
--device-read-bps
--device-write-bps
--device-read-iops
--device-write-iopsSyntax looks like --device-read-bps /dev/sda:10mb. Use a disposable loop device or test volume — do not benchmark against production filesystems. This lab did not run device-level tests because no safe throwaway block device was configured.
Rootless resource limits on cgroup v2
Rootless limits work when the user systemd session delegates controllers. On user golinuxcloud after enabling linger:
cat /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/cgroup.controllersSample output:
cpu memory pidsA constrained rootless container started successfully:
podman run --rm --cpus 0.5 --memory 256m quay.io/libpod/alpine_nginx:latest echo okSample output:
okDelegation may omit io or cpuset at the user slice — flags that need missing controllers will fail even when memory and CPU work.
Fix the requested cgroup controller cpu is not available
When systemd has not delegated a controller, crun returns errors such as:
crun: the requested cgroup controller `cpu` is not availableor for cpuset:
crun: the requested cgroup controller `cpuset` is not availableThe engine cannot write cpu.max or cpuset.cpus if cpu or cpuset is absent from the user’s cgroup.controllers file. That is a delegation problem, not a Podman bug.
Delegate resource controllers to users
Administrators can extend delegation with a systemd drop-in such as /etc/systemd/system/user@.service.d/delegate.conf:
[Service]
Delegate=memory pids cpu cpusetReload systemd and refresh the user manager session:
systemctl daemon-reloadEnd the user manager session so systemd recreates it with the new delegation — replace USER with the account name:
loginctl terminate-user USERThe user must log in again (or rely on linger) before cgroup.controllers lists the new entries. Only delegate controllers your policy intends users to control — this article does not replace full systemd delegation design.
Rootless cgroup v1 limitations
On rootless cgroup v1, many resource controls are unsupported or incomplete, including memory limits, CPU quota and shares, cpuset, and blkio tuning. RHEL 10 defaults to cgroup v2; keep v1 notes for older hosts still migrating.
Set pod-level resource limits
Pod limits apply to the shared pod cgroup on Podman 5.8.2:
podman pod create --name limited-pod --cpus 1 --memory 1gAttach a member container:
podman run -d --pod limited-pod --name limited-worker docker.io/library/alpine:latest sleep 3600List pod cgroup paths — limits apply on the pod slice, not inside each member cgroup:
podman pod inspect limited-pod --format '{{.CgroupParent}}'Sample output:
machine.sliceOn cgroup v2 the pod slice path looks like machine-libpod_pod_<POD_ID>.slice. Read limits on that slice:
cat /sys/fs/cgroup/machine.slice/machine-libpod_pod_$(podman pod inspect limited-pod --format '{{.Id}}').slice/memory.maxSample output:
1073741824Inspect results for the pod slice versus the worker container:
| Cgroup | memory.max |
cpu.max |
|---|---|---|
| Pod slice | 1073741824 (1 GiB) |
100000 100000 (1 CPU) |
| Worker container | max |
max 100000 |
Pod-level flags constrain the group; member cgroups may still show max for memory while the slice enforces the shared ceiling. Individual containers can set stricter limits than the pod ceiling; the pod limit remains the shared upper bound for members using the pod cgroup hierarchy. Pod concepts beyond limits live in Podman pods.
Resource limits with Quadlet
Quadlet .container units accept resource directives without repeating podman run flags on the command line. Examples:
Memory=512M
PidsLimit=256CPU can be expressed through unit settings that map to cgroup limits. Full syntax and systemd integration are covered in Podman Quadlet container file — this page only shows that the same limits can be declared declaratively.
Verify every limit you set
Do not stop at podman run --memory … and assume success. For each limit:
podman inspect—HostConfigfields in bytes or structured values- cgroup v2 files —
memory.max,cpu.max,pids.max,cpuset.cpus.effective,io.bfq.weight - podman stats — live CPU or memory behavior under workload
That three-step loop is what separates configuration from proof.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
cgroup controller cpu is not available rootless |
CPU not delegated to user slice | Add Delegate=cpu (and reload) or run rootful |
memory.max is max after --memory |
Wrong cgroup path inspected | Read container cgroup, not parent slice |
--cpu-shares “ignored” when alone |
No competing containers | Shares apply under contention only |
Fork failures with --pids-limit |
PID cgroup full | Expected; raise limit or reduce processes |
| Pod limit not visible on container cgroup | Limits on pod slice | Inspect machine-libpod_pod_….slice |
blkio.weight missing on v2 |
BFQ uses io.bfq.weight |
Read io.bfq.weight instead |
References
- podman-run(1) — resource options — memory, CPU, pids, cpuset, and blkio flags
- podman-pod-create(1) — pod-level
--cpusand--memory - Red Hat — Building, running, and managing containers — resource controls on RHEL-family systems
- systemd.resource-control(5) — cgroup property semantics shared with containers
Summary
Podman resource limits are cgroup limits. --memory becomes memory.max, --cpus becomes cpu.max, and --pids-limit becomes pids.max when you verify on cgroup v2. Always cross-check podman inspect against the files under /sys/fs/cgroup discovered from the container PID — IDs change, the verification pattern does not.
Hard caps (--memory, --cpus) behave differently from relative weights (--cpu-shares, --blkio-weight). CPU sets pin eligible processors without assigning a time percentage. --memory-swap counts memory plus swap together, not swap alone.
Rootless limits on cgroup v2 need systemd to delegate cpu, memory, and pids into the user slice; missing delegation produces crun controller errors. Pod limits on Podman 5.8.2 apply to the pod slice while member containers may still show max locally. After limits are in place, use podman stats to watch utilization — this guide sets the ceiling; stats shows how close you are to it.

