Skip to main content
RunBook Academy

Docker & ContainersXV Β· Resource ControlsPIDs

PIDs limit and process controls

Foundation⏱ ~22 min

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

Not yet marked complete on this device.

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

Configuration changepids limit
docker run -d --name api --pids-limit 200 myorg/api:1.0.0
services:
  api:
    image: myorg/api:1.0.0
    pids_limit: 200
Read-only / Safeverify the limit is in force
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.peak

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

Read-only / Safeprocesses versus tasks
$ 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.current
4
287

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

RuntimeWhat you see
JVMjava.lang.OutOfMemoryError: unable to create native thread
PythonOSError: [Errno 11] Resource temporarily unavailable
Goruntime: failed to create new OS thread
Node.jsError: spawn EAGAIN
Shellfork: retry: Resource temporarily unavailable
Read-only / Safehow close is every container to its ceiling
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
Read-only / Safethe counter that proves it happened
$ PID=$(docker inspect --format '{{.State.Pid}}' api); CG=$(awk -F: '/^0::/{print $3}' "/proc/$PID/cgroup"); cat /sys/fs/cgroup"$CG"/pids.events
max 1477

Illustrative 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_connections plus 30 for the postmaster, autovacuum workers, background writer and WAL processes. With max_connections = 100, 200 is comfortable.
  • Build and test containers. 2000+. A parallel make -j or 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

  1. Q1. A JVM container fails with `java.lang.OutOfMemoryError: unable to create native thread`. What should you check first?

  2. Q2. A container shows 4 processes in `ps -e` but pids.current of 287. Why?

  3. Q3. Exceeding pids.max makes fork() and clone() return EAGAIN; the kernel kills nothing.

  4. Q4. Which statements about PID limits and the host are correct? Select all that apply.

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