Set CPU and Memory Limits for Podman Containers

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:

bash
podman info --format '{{.Host.CgroupsVersion}} {{.Host.CgroupManager}}'

Sample output on RHEL 10.2:

output
v2 systemd

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

bash
podman inspect --format '{{.State.Pid}}' CONTAINER

Follow /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:

bash
podman run -d --name mem-limit-demo --memory 512m docker.io/library/alpine:latest sleep 3600

Confirm Podman stored the byte value:

bash
podman inspect --format '{{.HostConfig.Memory}}' mem-limit-demo

Sample output:

output
536870912

536870912 bytes is 512 MiB. Turn the cgroup line from /proc into a path under /sys/fs/cgroup and read the hard limit:

bash
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' mem-limit-demo)/cgroup)/memory.max"

Sample output:

output
536870912

memory.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:

bash
podman run -d --name mem-reserve --memory 512m --memory-reservation 256m docker.io/library/alpine:latest sleep 3600

Inspect both values:

bash
podman inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}}' mem-reserve

Sample output:

output
536870912 268435456

On the cgroup path, memory.low matched the reservation bytes:

bash
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' mem-reserve)/cgroup)/memory.low"

Sample output:

output
268435456

memory.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”:

bash
podman run -d --name mem-swap --memory 512m --memory-swap 1g docker.io/library/alpine:latest sleep 3600

Inspect totals:

bash
podman inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemorySwap}}' mem-swap

Sample output:

output
536870912 1073741824

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

bash
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' mem-swap)/cgroup)/memory.swap.max"

Sample output:

output
536870912

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

bash
podman run -d --name cpu-limit --cpus 0.5 docker.io/library/alpine:latest sh -c 'while true; do :; done'

Inspect the nanosecond representation:

bash
podman inspect --format '{{.HostConfig.NanoCpus}}' cpu-limit

Sample output:

output
500000000

Read cgroup v2 quota and period from the same discovery pattern:

bash
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' cpu-limit)/cgroup)/cpu.max"

Sample output on the lab container:

output
50000 100000

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

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

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

bash
podman stats --no-stream --format '{{.Name}} {{.CPU}}' cpu-share-high cpu-share-low

Sample output on a three-core host:

output
cpu-share-high 94.26
cpu-share-low 65.43

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

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

bash
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' cpuset-demo)/cgroup)/cpuset.cpus.effective"

Sample output:

output
0

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

bash
lscpu | grep 'NUMA node(s)'

Sample output:

output
NUMA node(s):                            1

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

bash
podman run -d --name pids-demo --pids-limit 20 docker.io/library/alpine:latest sleep 3600

Confirm the cgroup ceiling:

bash
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' pids-demo)/cgroup)/pids.max"

Sample output:

output
20

Try to exceed the cap:

bash
podman exec pids-demo sh -c 'for i in $(seq 1 25); do sleep 3600 & done'

Sample output:

output
sh: fork: retry: Resource temporarily unavailable
sh: fork: Resource temporarily unavailable

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

bash
podman run -d --name blkio-demo --blkio-weight 500 docker.io/library/alpine:latest sleep 3600

On cgroup v2 this host exposes BFQ weight instead of legacy blkio.weight:

bash
cat "/sys/fs/cgroup$(cut -d: -f3 /proc/$(podman inspect --format '{{.State.Pid}}' blkio-demo)/cgroup)/io.bfq.weight"

Sample output:

output
default 500

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

text
--device-read-bps
--device-write-bps
--device-read-iops
--device-write-iops

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

bash
cat /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/cgroup.controllers

Sample output:

output
cpu memory pids

A constrained rootless container started successfully:

bash
podman run --rm --cpus 0.5 --memory 256m quay.io/libpod/alpine_nginx:latest echo ok

Sample output:

output
ok

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

text
crun: the requested cgroup controller `cpu` is not available

or for cpuset:

text
crun: the requested cgroup controller `cpuset` is not available

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

ini
[Service]
Delegate=memory pids cpu cpuset

Reload systemd and refresh the user manager session:

bash
systemctl daemon-reload

End the user manager session so systemd recreates it with the new delegation — replace USER with the account name:

bash
loginctl terminate-user USER

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

bash
podman pod create --name limited-pod --cpus 1 --memory 1g

Attach a member container:

bash
podman run -d --pod limited-pod --name limited-worker docker.io/library/alpine:latest sleep 3600

List pod cgroup paths — limits apply on the pod slice, not inside each member cgroup:

bash
podman pod inspect limited-pod --format '{{.CgroupParent}}'

Sample output:

output
machine.slice

On cgroup v2 the pod slice path looks like machine-libpod_pod_<POD_ID>.slice. Read limits on that slice:

bash
cat /sys/fs/cgroup/machine.slice/machine-libpod_pod_$(podman pod inspect limited-pod --format '{{.Id}}').slice/memory.max

Sample output:

output
1073741824

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

ini
Memory=512M
PidsLimit=256

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

  1. podman inspectHostConfig fields in bytes or structured values
  2. cgroup v2 files — memory.max, cpu.max, pids.max, cpuset.cpus.effective, io.bfq.weight
  3. 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


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.


Frequently Asked Questions

1. What is the difference between podman --cpus and --cpu-shares?

--cpus sets a hard CPU quota expressed as a fraction of one CPU, enforced through cgroup cpu.max. --cpu-shares sets relative scheduling weight when multiple containers compete for CPU — a low-share container can still use all available CPU when nothing else is busy. Use --cpus for a cap; use shares only for contention policy.

2. Does podman --memory-swap set swap size only?

No. --memory-swap is the combined memory plus swap ceiling, not swap alone. With --memory 512m and --memory-swap 1g, the container can use about 512 MiB RAM and about 512 MiB additional swap when the host provides it. Inspect HostConfig.MemorySwap stores the total in bytes.

3. How do I verify a Podman memory limit was applied?

Check podman inspect --format '{{.HostConfig.Memory}}' CONTAINER for bytes, then read memory.max under the container cgroup v2 directory discovered from podman inspect --format '{{.State.Pid}}' and /proc/PID/cgroup. On the lab host 512m became 536870912 in both places.

4. Why does rootless podman fail with cgroup controller cpu is not available?

systemd has not delegated the cpu controller into the user cgroup hierarchy. The container runtime cannot configure cpu.max when the user slice lacks cpu in cgroup.controllers. An administrator can add Delegate=memory pids cpu cpuset to user@.service.d and reload systemd, then restart the user session.

5. Where do pod-level CPU and memory limits apply?

On Podman 5.8.2 with cgroup v2, pod create --cpus and --memory set limits on the pod slice cgroup. Member containers may show memory.max and cpu.max as max inside their own cgroups while the pod slice enforces the shared ceiling. Inspect the pod slice path, not only individual container cgroups.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years 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, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)