Container memory headroom calculator
Work out whether a Docker host will OOM, and which container dies first, using observed working sets rather than configured limits. Defaults are measured from a production host that OOM-killed.
Why summing limits tells you nothing
Every guide to Docker memory limits performs the same arithmetic: add up mem_limit across your services and compare it to host RAM. It is the obvious calculation and it is close to useless, because a limit is a ceiling, not a reservation. A container with a 5 GB limit using 13 MB consumes 13 MB.
The defaults in this calculator are not illustrative. They are measured from a production host that OOM-killed on 9 August 2026, with names changed.
What actually happened on that host
Three API replicas were configured at 1536 MB. The kernel killed them roughly every two minutes:
Memory cgroup out of memory: Killed process (api) anon-rss:1239516kB
Memory cgroup out of memory: Killed process (api) anon-rss:1224876kB
Memory cgroup out of memory: Killed process (api) anon-rss:1268664kB
Three details in there are worth more than any guide:
- The kill happens at roughly 1.21 GB resident, not at 1536 MB. The limit counts page cache and other charges, so the process dies well before its nominal ceiling.
constraint=CONSTRAINT_MEMCGmeans the container's limit was hit, not the host's memory. Those are different failures with different fixes.- Each kill produced a burst of 2,000 to 5,000 502s per minute at the proxy. The user-visible symptom was a connection error, three layers away from the cause.
The trap: raising the limit made it worse
The limit was raised from 1536 MB to 2560 MB. The kills stopped immediately. Two hours later:
api-2 2367 MB / 2560 MB headroom 7.5%
api-3 1940 MB / 2560 MB headroom 24.2%
api-1 1892 MB / 2560 MB headroom 26.1%
The processes leaked. They grew into whatever space they were given. Three of them took 6.2 GB, host free memory fell to 1 GB, swap filled to 4091 of 4096 MB, and load average reached 65 on six cores.
The cgroup limit had been doing something useful: it was killing a leaking process before it could take the host down. Raising it removed that protection.
A rolling restart proved the memory was garbage rather than working set:
api-1 1.948 GiB -> 122 MiB
api-2 1.231 GiB -> 367 MiB
api-3 2.048 GiB -> 333 MiB
Nothing about the workload changed. Those gigabytes were never needed.
The oversubscription number is usually meaningless
That host committed 28,864 MB of limits against 11,960 MB of RAM — a 2.41× oversubscription that sounds alarming and mostly was not, because the ceilings were fictional:
| Container | Limit | Actually used |
|---|---|---|
| headless-browser | 5120 MB | 13 MB |
| proxy | 4096 MB | 257 MB |
| control-plane | 1024 MB | 15 MB |
| scheduler | 1024 MB | 48 MB |
Roughly 11.8 GB of ceiling that no process ever approached. Lowering those limits frees no memory at all. It is still worth doing, so that the oversubscription figure becomes a number you can plan against instead of one you learn to ignore.
How to use this
Get your real figures with two commands:
docker stats --no-stream --format '{{.Name}} {{.MemUsage}}'
dmesg -T | grep 'Killed process'
For the growth column, note a container's working set, wait an hour, note it again. If it climbs on steady traffic, you have a leak, and the honest options are to fix it or to recycle on a schedule until you can.
What this calculator will not tell you
- Whether you have a leak. It takes your growth figure on trust. Only observation over time establishes that.
- Your real kill point. Cgroup accounting includes page cache and kernel memory, so the resident size at death is lower than the limit by an amount that depends on your workload. The measurements above put it near 79% of the limit; yours will differ.
- Anything about CPU. The host above was also CPU-saturated at load 16 after the memory was fixed. Memory headroom and CPU headroom are separate problems and fixing one exposes the other.
Reviewed by Krishna on 2026-08-09.