The core idea
Without limits, a container can use all of the host's memory and CPU. One leaking process then slows or kills
everything else on the server. Limits are set per container with docker run flags, and the kernel enforces them
through cgroups. The two that matter most behave very differently: memory is a hard wall, CPU is a throttle.
Memory: a hard wall
$ docker run --name hog --memory 32m node:22-slim node -e 'const a=[];while(true)a.push(Buffer.alloc(1e6,1))'$ docker inspect -f '{{.State.Status}} {{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}' hogexited 137 OOMKilled=true
At the limit, the kernel can't give out more memory, so its OOM killer ends a process in the container. There's no slowing down first.
| Flag | Means |
|---|---|
--memory 512m | hard limit on RAM |
--memory-swap 512m | RAM + swap combined; set it equal to --memory to allow no swap |
--memory-reservation 256m | a soft target the kernel pushes towards when the host is short on memory |
CPU: a throttle
$ docker run -d --name burner --cpus 0.5 node:22-slim node -e 'for(;;){}'$ docker stats --no-stream burner --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'NAME CPU % MEM USAGE / LIMITburner 50.53% 7.484MiB / 7.653GiB
An infinite loop would happily use a whole core. With --cpus 0.5 it gets half of one, and is simply made to wait
for the rest. Nothing is killed; the app just runs slower.
| Flag | Means |
|---|---|
--cpus 1.5 | at most one and a half cores' worth of CPU time |
--cpu-shares 512 | relative weight when CPUs are contended (default 1024); no effect when the host is idle |
--cpuset-cpus 0,1 | pin to specific cores |
Note the memory column: with no --memory, the limit shown is everything Docker has, here the Docker Desktop VM's
7.65 GiB.
Processes: a fork-bomb guard
$ docker run --rm --pids-limit 20 alpine sh -c 'for i in $(seq 1 30); do sleep 5 & done; wait'sh: can't fork: Resource temporarily unavailable
--pids-limit caps how many processes and threads a container can create.
Does your app know about the limit?
A limit only helps if the runtime inside respects it. Node sizes its heap from available memory, and recent versions read the cgroup limit:
$ docker run --rm node:22-slim node -e 'console.log(Math.round(require("v8").getHeapStatistics().heap_size_limit/1048576))'2096$ docker run --rm --memory 512m node:22-slim node -e 'console.log(Math.round(require("v8").getHeapStatistics().heap_size_limit/1048576), Math.round(process.constrainedMemory()/1048576))'259 512
Under a 512 MB limit, Node 22 caps its heap at 259 MB, leaving room for buffers and native memory. Not every runtime
does this: older Java (before 10) and older Node versions ignored the cgroup limit and sized themselves from the
host's memory, then got OOM-killed. Check yours the same way before trusting it. If yours does, set it explicitly: NODE_OPTIONS=--max-old-space-size=384, Java's
-XX:MaxRAMPercentage, or a worker count tied to --cpus.
A container with --memory 256m --cpus 0.25 starts doing heavy work. Which is true?
Run the notes API with --memory 128m --cpus 0.5 --pids-limit 100 and check all three in docker stats --no-stream and docker inspect. Then use docker update --memory 256m --memory-swap 256m notes to raise the memory limit on the running container.
STUCK? ▸
docker stats shows no limit: you passed the flags after the image name, so they went to Node instead of Docker. Flags go before the image.
Recap
Interview questions
How do memory and CPU limits behave differently when a container exceeds them?
A Java (or old Node) service keeps getting OOM-killed in a container even though its heap setting looks fine. Why?