Docker & ContainersXV Β· Resource ControlsPIDs
PIDs limit and process controls
What you'll learn
- Configure PIDs limits
- Recognise fork-bomb and thread-leak scenarios
- Diagnose PID exhaustion from the cgroup rather than the application
- Choose a limit that contains a runaway without breaking normal load
Prerequisites
None β start here.
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-09
--pids-limit caps the number of processes and threads a
container may have alive at once. It is the cheapest limit in Docker
β one integer, no measurement required to get it approximately right
β and the only one whose absence can take a whole host down in
seconds rather than minutes.
Docker sets no limit by default. There is no default-pids-limit in
daemon.json; if you do not pass the flag, the containerβs
pids.max is max.
Setting it
docker run -d --name api --pids-limit 200 myorg/api:1.0.0services:
api:
image: myorg/api:1.0.0
pids_limit: 200
CID=api
PID=$(docker inspect --format '{{.State.Pid}}' "$CID")
CG=$(awk -F: '/^0::/{print $3}' "/proc/$PID/cgroup")
cat /sys/fs/cgroup"$CG"/pids.max
cat /sys/fs/cgroup"$CG"/pids.current
cat /sys/fs/cgroup"$CG"/pids.peakpids.peak is the useful one for sizing: it is the high-water mark
since the cgroup was created, so it tells you what the workload has
actually needed rather than what it is using right now.
Threads count, and that is the whole sizing problem
pids.max counts tasks, which in Linux means threads. A
single-process JVM with 300 threads consumes 300 against the limit.
ps -e shows one process; the cgroup sees 300.
This is where a limit that seemed generous turns out not to be.
$ docker exec api sh -c 'ps -e | wc -l'; PID=$(docker inspect --format '{{.State.Pid}}' api); CG=$(awk -F: '/^0::/{print $3}' "/proc/$PID/cgroup"); cat /sys/fs/cgroup"$CG"/pids.current4
287Illustrative output
Four processes, 287 tasks. A --pids-limit 50 chosen by counting
processes would have killed this container on start.
Fork bombs, and the thing that is far more common
The textbook case is a fork bomb: a construct that recursively
spawns processes until the process table is full. On a host with no
PID limits, dockerd, sshd and every other service lose the
ability to fork(), which means you cannot log in to fix it. The
recovery is a reboot.
In production, deliberate fork bombs are rare and thread leaks are not. The realistic ways a container reaches its PID ceiling:
- A connection pool or executor configured with an unbounded maximum that grows one thread per stuck request during a downstream outage.
- A retry loop that spawns a subprocess per attempt and does not reap them, leaving zombies that still occupy PIDs.
- A shell entrypoint that backgrounds a helper on every iteration of a loop.
- A crash loop inside the container where a supervisor restarts a child faster than the old one exits.
All four are gradual. pids.current climbs over hours, and the
limit converts a slow leak into a bounded failure at a predictable
point rather than an unbounded one that spreads.
What hitting the limit looks like
The kernel returns EAGAIN from fork() and clone(). It does not
kill anything. What happens next depends entirely on how the
application handles a failed thread creation, and the error messages
are misleading in a specific and expensive way:
| Runtime | What you see |
|---|---|
| JVM | java.lang.OutOfMemoryError: unable to create native thread |
| Python | OSError: [Errno 11] Resource temporarily unavailable |
| Go | runtime: failed to create new OS thread |
| Node.js | Error: spawn EAGAIN |
| Shell | fork: retry: Resource temporarily unavailable |
for c in $(docker ps -q); do
name=$(docker inspect --format '{{.Name}}' "$c" | tr -d /)
pid=$(docker inspect --format '{{.State.Pid}}' "$c")
cg=$(awk -F: '/^0::/{print $3}' "/proc/$pid/cgroup")
cur=$(cat /sys/fs/cgroup"$cg"/pids.current)
max=$(cat /sys/fs/cgroup"$cg"/pids.max)
printf '%-24s %s / %s\n' "$name" "$cur" "$max"
done$ PID=$(docker inspect --format '{{.State.Pid}}' api); CG=$(awk -F: '/^0::/{print $3}' "/proc/$PID/cgroup"); cat /sys/fs/cgroup"$CG"/pids.eventsmax 1477Illustrative output
That counter is the closest thing to a smoking gun in this lesson.
A container that hit its limit at 04:00 and recovered still shows a
non-zero max here at 09:00, long after pids.current has fallen
back to normal β which is exactly the evidence an overnight incident
otherwise lacks.
Setting the right limit
The limit must be above the workloadβs peak task count under normal load, and well below the point where the host suffers. Starting points:
- Web / API services. 200β500 covers most Node, Python and Go services. A JVM service is usually 300β1000 depending on pool configuration β measure it rather than guessing.
- PostgreSQL. Roughly
max_connectionsplus 30 for the postmaster, autovacuum workers, background writer and WAL processes. Withmax_connections = 100, 200 is comfortable. - Build and test containers. 2000+. A parallel
make -jor a test runner forks hard by design, and the workload is bounded by CPU anyway. - Sidecars and exporters. 50β100. These should be small, and a low limit on them is a cheap tripwire.
The method that actually works: run the workload through a peak
cycle with a generous limit, read pids.peak, and set the limit to
roughly twice it. Then leave it. The purpose of the number is to
bound a runaway, not to be a tight fit.
Knowledge check
Knowledge check Β· 5 questions
Q1. A JVM container fails with `java.lang.OutOfMemoryError: unable to create native thread`. What should you check first?
Q2. A container shows 4 processes in `ps -e` but pids.current of 287. Why?
Q3. Exceeding pids.max makes fork() and clone() return EAGAIN; the kernel kills nothing.
Q4. Which statements about PID limits and the host are correct? Select all that apply.
Q5. Which cgroup file shows that a container hit its PID limit hours ago, after pids.current has returned to normal?
Passing score: 75%. Answers are checked in this browser.