The Problem
A container is slow, the host is hot, and you have no idea which service is to blame. Guessing wastes time. docker stats gives you a live, top-like view of CPU, memory, network, and disk I/O for every running container — straight from the kernel's cgroup accounting, no agents to install. Learning to read its columns turns "something is wrong" into "the worker container is pinned at its memory limit and getting OOM-killed."
The Basic Command
# Live, auto-refreshing view of all running containers
docker stats
# Just specific containers
docker stats web db worker
By default it refreshes continuously, like top. Press Ctrl+C to exit.
Reading Every Column
| Column | What it means |
|---|---|
| <strong>CONTAINER ID / NAME</strong> | Which container the row describes |
| <strong>CPU %</strong> | CPU used relative to the container's allocation. With <code class="inline-code">--cpus=2</code>, 200% means both cores fully used |
| <strong>MEM USAGE / LIMIT</strong> | Current memory vs the configured limit (or host total if no limit set) |
| <strong>MEM %</strong> | Usage as a percentage of the limit |
| <strong>NET I/O</strong> | Total bytes received / sent over the network since start |
| <strong>BLOCK I/O</strong> | Total bytes read / written to block devices (disk) |
| <strong>PIDS</strong> | Number of processes/threads the container is running |
A couple of nuances worth knowing: CPU % can exceed 100% on multi-core allocations, and MEM USAGE normally excludes reclaimable page cache so it tracks real working-set memory.
--no-stream for Scripts
The live view is great for a human but useless in a script because it never exits. Add --no-stream to print one snapshot and return.
# Single snapshot, then exit — perfect for cron or CI
docker stats --no-stream
Formatting the Output
Use --format with a Go template to get exactly the columns you want — ideal for logging or feeding another tool.
# Compact table
docker stats --no-stream \
--format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
For machine parsing, emit JSON lines:
docker stats --no-stream --format '{{json .}}'
{"BlockIO":"1.2MB / 8.19MB","CPUPerc":"14.03%","Container":"web","MemPerc":"35.16%","MemUsage":"180MiB / 512MiB","Name":"web","NetIO":"4.1kB / 3.2kB","PIDS":"18"}
Pipe that into jq to extract a single metric:
docker stats --no-stream --format '{{json .}}' \
| jq -r 'select(.Name=="worker") | .MemPerc'
Relation to cgroup Limits
docker stats numbers only make sense against the limits you set. Those limits are enforced by the kernel's cgroups and configured at run time.
# Cap memory at 512MB and CPU at 1.5 cores
docker run -d --name worker \
--memory=512m \
--cpus=1.5 \
myapp:1.0
--memory=512msets the LIMIT column. Hit it and the kernel OOM-kills the container (exit code 137).--cpus=1.5means the CPU % column is relative to 150% of one core. Sustained max usage means the container is being throttled.
Without limits, a single container can starve its neighbors — stats will show it, but prevention comes from setting the limits.
Spotting a Leaking or Throttled Container
Memory leak: MEM USAGE climbs steadily toward the LIMIT and never falls back during idle periods. Eventually the container is OOM-killed and restarts, resetting the number — a sawtooth pattern across restarts is a classic leak signature.
# Sample memory every 5s to watch the trend
while true; do
docker stats --no-stream --format '{{.Name}} {{.MemUsage}} {{.MemPerc}}' worker
sleep 5
done
CPU throttling: CPU % sits at the --cpus ceiling (for example a steady 150% with --cpus=1.5) while latency rises. Confirm with the kernel's throttling counters:
docker inspect --format '{{.State.Pid}}' worker
# Check nr_throttled / throttled_time under the container's cgroup cpu.stat
cat /sys/fs/cgroup/system.slice/docker-*/cpu.stat 2>/dev/null | grep throttled
OOM confirmation: if a container keeps vanishing, check its exit code and the OOMKilled flag.
docker inspect worker --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
# true 137 → killed for exceeding its memory limit
Common Mistakes to Avoid
- Running plain
docker statsin a script. It streams forever and the script hangs. Always add--no-streamfor automation. - Reading CPU % as a share of the whole host. It is relative to the container's
--cpusallocation, so it can legitimately exceed 100%. - Treating MEM USAGE as all RAM in use. It generally excludes reclaimable cache; do not panic at page cache, panic when the working set approaches the LIMIT.
- Monitoring without limits. Stats tell you a container is greedy, but only
--memoryand--cpusstop it from hurting neighbors. - Using
docker statsas long-term monitoring. It is a point-in-time tool. For history and alerting, export metrics to Prometheus/cAdvisor instead.
FAQ
What does CPU % over 100% mean in docker stats?
It means the container is using more than one CPU core's worth of time. CPU % is relative to the cores available to the container, so with --cpus=4 a value of 320% is normal and healthy. It only indicates a problem if it is pinned at the configured ceiling while performance suffers.
Why does MEM USAGE look lower than I expect?
docker stats typically reports the working set and excludes reclaimable page cache, which the kernel can free under pressure. That gives a truer picture of memory the container actually needs. Watch the trend relative to the LIMIT rather than the absolute number.
How do I use docker stats in a monitoring script?
Add --no-stream so it prints one snapshot and exits, and --format to output only the fields you need (or {{json .}} for structured output). Pipe the result into jq or a logging pipeline. For continuous history, schedule periodic snapshots or use a dedicated exporter.
How is docker stats related to --memory and --cpus?
Those flags set the cgroup limits that the LIMIT column reflects and that CPU % is measured against. Exceeding --memory triggers an OOM kill (exit 137); hitting --cpus causes throttling. Without limits, stats still report usage, but there is nothing stopping one container from monopolizing the host.