Skip to main content
Docker·7 min read

docker stats — Monitor Container CPU, Memory, and I/O in Real Time

A practical guide to docker stats for container resource monitoring: every column explained, --no-stream for scripts, custom --format output, how stats relate to cgroup limits (--memory/--cpus), and how to spot a leaking or throttled container.

DT

DevOps Engineer & Technical Writer

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

docker stats — LIVE CONTAINER RESOURCE DASHBOARD CONTAINER CPU % MEM USAGE / LIMIT PIDS web healthy 14% 180MiB / 512MiB 18 db busy 78% 770MiB / 1GiB 42 worker leaking! 53% 505MiB / 512MiB (OOM soon) 7 MEM near LIMIT + climbing = leak or undersized limit → next step is a container restart (OOM kill) CPU % is relative to the --cpus allocation; sustained 100% means the container is CPU-bound or throttled

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

ColumnWhat 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=512m sets the LIMIT column. Hit it and the kernel OOM-kills the container (exit code 137).
  • --cpus=1.5 means 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 stats in a script. It streams forever and the script hangs. Always add --no-stream for automation.
  • Reading CPU % as a share of the whole host. It is relative to the container's --cpus allocation, 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 --memory and --cpus stop it from hurting neighbors.
  • Using docker stats as 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.

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.