Skip to content

Docker Resource Limits — CPU, Memory, and I/O Constraints Guide

DodaTech Updated 2026-06-24 8 min read

In this tutorial, you'll learn about Docker Resource Limits. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Docker resource limits constrain how much CPU, memory, and I/O a container can consume, preventing a single container from starving others on the same host.

What You'll Learn

You'll master Docker resource constraints — memory limits and reservations, CPU shares vs quotas, block I/O throttling, kernel memory, and cgroups v2 configuration for production workloads.

Why This Problem Matters

Without limits, one container can consume all host memory and trigger the OOM killer, taking down every container on the host. Unbounded CPU usage causes noisy-neighbor problems. Resource limits ensure predictable performance and fair sharing.

Real-World Use

DodaZIP's file processing pipeline runs 50 worker containers per host. Each worker is limited to 512MB memory and 0.5 CPU cores. When a single file causes a memory spike in one worker, the OOM killer terminates only that worker — the other 49 continue processing.

Resource Limit Architecture

flowchart TB
  Host[Linux Host] --> Cgroups[Control Groups v2]
  Cgroups --> CPUSubsys[CPU Controller]
  Cgroups --> MemorySubsys[Memory Controller]
  Cgroups --> IOSubsys[Block I/O Controller]
  Cgroups --> PIDSSubsys[PID Controller]
  
  subgraph Container1
    Proc1[app process]
    Proc1 --> |cgroup limits| CPUSubsys
    Proc1 --> |cgroup limits| MemorySubsys
  end
  
  subgraph Container2
    Proc2[app process]
    Proc2 --> |cgroup limits| CPUSubsys
    Proc2 --> |cgroup limits| MemorySubsys
  end
  
  MemorySubsys -->|OOM if exceeded| OOM[OOM Killer]

Memory Limits

# Hard limit: container cannot exceed 512MB
docker run -d --name limited-web \
  --memory="512m" \
  nginx

# Soft limit: container tries to stay under 256MB, can burst
docker run -d --name burstable-web \
  --memory="512m" --memory-reservation="256m" \
  nginx

# Swap limit: allow up to 1GB of swap
docker run -d --name with-swap \
  --memory="512m" --memory-swap="1g" \
  nginx

# Disable swap entirely
docker run -d --name no-swap \
  --memory="512m" --memory-swap="512m" \
  nginx
import docker
import time

client = docker.from_env()

# Run a container that will hit memory limit
container = client.containers.run(
    "python:3.12-slim",
    "python -c \""
    "data = []\n"
    "while True:\n"
    "    data.append('x' * 1024 * 1024)\n"
    "\"",
    mem_limit="100m",
    detach=True
)

time.sleep(10)
container.reload()
print(f"Status: {container.status}")
print(f"OOM killed: {container.attrs['State'].get('OOMKilled')}")
container.remove(force=True)

Expected output:

Status: exited
OOM killed: True

The container is killed by the OOM killer when it exceeds 100MB.

CPU Limits

# CPU shares (relative weight, default 1024)
docker run -d --name high-priority --cpu-shares=2048 nginx
docker run -d --name low-priority --cpu-shares=512 nginx

# CPU quota (max CPU time per period)
docker run -d --name half-core --cpus="0.5" nginx

# CPU pinning to specific cores
docker run -d --name pinned --cpuset-cpus="0,2" nginx

# CPU quota with explicit period
docker run -d --name quota \
  --cpu-quota=50000 --cpu-period=100000 \
  nginx
import docker
import time
import multiprocessing

client = docker.from_env()

# Run a CPU stress test
container = client.containers.run(
    "python:3.12-slim",
    "python -c \""
    "import multiprocessing\n"
    "def burn():\n"
    "    while True:\n"
    "        pass\n"
    "procs = []\n"
    "for i in range(multiprocessing.cpu_count()):\n"
    "    p = multiprocessing.Process(target=burn)\n"
    "    p.start()\n"
    "    procs.append(p)\n"
    "for p in procs:\n"
    "    p.join()\n"
    "\"",
    nano_cpus=int(0.5 * 1e9),  # 0.5 CPU
    detach=True
)

time.sleep(5)
container.reload()

# Check CPU usage
stats = container.stats(stream=False)
cpu_delta = stats["cpu_stats"]["cpu_usage"]["total_usage"]
system_delta = stats["cpu_stats"]["system_cpu_usage"]
cpu_percent = (cpu_delta / system_delta) * 100 * multiprocessing.cpu_count()
print(f"CPU usage: {cpu_percent:.1f}% (limited to 50%)")
container.remove(force=True)

Expected output:

CPU usage: 49.8% (limited to 50%)

Block I/O Limits

# Read/write BPS limits
docker run -d --name io-limited \
  --device-read-bps /dev/sda:1mb \
  --device-write-bps /dev/sda:1mb \
  nginx

# Read/write IOPS limits
docker run -d --name iops-limited \
  --device-read-iops /dev/sda:100 \
  --device-write-iops /dev/sda:50 \
  nginx

PID Limits

Prevent fork bombs from exhausting host PIDs:

# Limit to 100 processes
docker run -d --name pid-limited \
  --pids-limit=100 \
  alpine sh -c "while true; do sleep 1 & done"

# Check how many PIDs are used
docker stats pid-limited --no-stream

Resource Limits with Compose

# docker-compose.yml
services:
  web:
    image: nginx
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 256M
        reservations:
          cpus: "0.25"
          memory: 128M

  worker:
    image: myapp/worker
    deploy:
      resources:
        limits:
          cpus: "1"
          memory: 512M
          pids: 200
    ulimits:
      nofile:
        soft: 1024
        hard: 2048

  database:
    image: postgres:16
    deploy:
      resources:
        limits:
          memory: 1G
    volumes:
      - pgdata:/var/lib/postgresql/data
    shm_size: 256m  # Increase /dev/shm for Postgres

Monitoring Resource Usage

import docker
import time

client = docker.from_env()

def monitor_container(container_id: str, duration: int = 30):
    container = client.containers.get(container_id)
    print(f"{'Time':>8} {'CPU%':>8} {'MemMB':>8} {'Mem%':>8} {'NetI':>8} {'NetO':>8}")
    print("-" * 56)

    end_time = time.time() + duration
    while time.time() < end_time:
        stats = container.stats(stream=False)
        cpu = stats["cpu_stats"]["cpu_usage"]["total_usage"]
        sys_cpu = stats["cpu_stats"]["system_cpu_usage"]
        precpu = stats["precpu_stats"]["cpu_usage"]["total_usage"]
        precpu_sys = stats["precpu_stats"]["system_cpu_usage"]
        cpu_delta = cpu - precpu
        sys_delta = sys_cpu - precpu_sys
        cpu_percent = (cpu_delta / sys_delta) * 100 if sys_delta > 0 else 0

        mem_usage = stats["memory_stats"]["usage"]
        mem_limit = stats["memory_stats"]["limit"]
        mem_mb = mem_usage / (1024 * 1024)
        mem_percent = (mem_usage / mem_limit) * 100

        net_rx = sum(
            net["rx_bytes"] for net in stats["networks"].values()
        ) if stats.get("networks") else 0
        net_tx = sum(
            net["tx_bytes"] for net in stats["networks"].values()
        ) if stats.get("networks") else 0

        print(
            f"{time.strftime('%H:%M:%S'):>8} "
            f"{cpu_percent:>7.1f}% "
            f"{mem_mb:>7.1f} "
            f"{mem_percent:>7.1f}% "
            f"{net_rx / 1024:>7.1f}KB "
            f"{net_tx / 1024:>7.1f}KB"
        )
        time.sleep(2)

Common Mistakes

1. Setting Memory but Not Swap

--memory="512m" allows the container to use 512MB RAM plus 512MB swap by default. The container appears to use 1GB before OOM. Set --memory-swap to the same value to disable swap.

2. Using CPU Shares Instead of Quotas

CPU shares only matter under contention. On an idle host, a --cpu-shares=512 container still gets 100% CPU. Use --cpus="0.5" for a hard limit.

3. No Memory Reservation

Without --memory-reservation, the container can burst to its hard limit at any time, potentially starving other containers. Set a reservation lower than the limit for predictable scheduling.

4. Confusing Host Memory with Container Memory

free -m inside a container shows host memory, not the container's limit. Use cat /sys/fs/cgroup/memory/memory.limit_in_bytes to see the actual limit.

5. Ignoring OOM Killer Impact

When a container exceeds memory, the kernel kills a Process inside it (not necessarily the main Process). The container may appear "running" but the application is dead. Always set health checks.

6. Over-limiting Kernel Memory

Setting --kernel-memory too low can cause kernel allocation failures. Only set kernel memory limits if you understand your application's kernel memory usage.

7. No ulimit Configuration

Process limits (nofile, nproc) are inherited from the host. Database containers often need higher file descriptor limits: --ulimit nofile=65536:65536.

Practice Questions

1. How does Docker enforce memory limits?

Docker uses cgroups v2 memory controller. The kernel tracks page counts per cgroup. When the limit is exceeded, the OOM killer terminates a Process in the cgroup. For swap, the kernel pages out to disk instead.

2. What is the difference between CPU shares and CPU quota?

Shares are relative weights (only matter under contention). Quota is a hard limit on CPU time per period (e.g., 50ms per 100ms period = 0.5 CPU). Quota guarantees the container never exceeds the limit.

3. How do --memory and --memory-swap interact?

--memory="512m" sets the RAM limit. --memory-swap="1g" sets RAM + swap = 1g (so swap = 512m). If --memory-swap equals --memory, swap is disabled entirely. If --memory-swap is unset, swap = 2x memory.

4. What happens when a container hits the OOM limit compared to the memory reservation?

Hitting the hard limit triggers OOM kill. The reservation is only used by the scheduler for placement — it's never enforced. A container can exceed its reservation but not its hard limit.

5. Challenge: Design a resource allocation Strategy for a multi-tenant CI runner.

Each CI job runs in a Docker container. Jobs have different resource requirements: some need 8GB for compilation, others need 256MB for tests. Design a Strategy that maximizes host utilization while preventing any single job from starving others.

Mini Project: Resource Limit Tester

#!/bin/bash
# test-resource-limits.sh

echo "=== Testing Memory Limit ==="
docker run --rm --memory=50m alpine \
  sh -c "echo 'Memory limit set to 50MB'; cat /sys/fs/cgroup/memory/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes"

echo ""
echo "=== Testing CPU Limit ==="
docker run --rm --cpus=0.5 alpine \
  sh -c "echo 'CPU limit set to 0.5 cores'; cat /sys/fs/cgroup/cpu/cpu.max 2>/dev/null || echo '50% quota'"

echo ""
echo "=== Testing OOM Behavior ==="
docker run --rm --memory=10m alpine \
  sh -c "echo 'Allocating 50MB...'; dd if=/dev/zero of=/dev/null bs=1M count=50" 2>&1 || echo "Container killed by OOM (expected)"

echo ""
echo "=== Test Complete ==="
chmod +x test-resource-limits.sh
./test-resource-limits.sh

Expected output:

=== Testing Memory Limit ===
Memory limit set to 50MB
52428800

=== Testing CPU Limit ===
CPU limit set to 0.5 cores
50000 100000

=== Testing OOM Behavior ===
Allocating 50MB...
Killed
Container killed by OOM (expected)

=== Test Complete ===

FAQ

What is the difference between cgroups v1 and v2 for Docker?

cgroups v2 (default on modern Linux) uses a unified hierarchy with a single mount point. Memory, CPU, and I/O controllers are managed through a single tree. Docker supports both, but v2 is recommended for new deployments.

Can I change resource limits on a running container?

Yes. docker update --memory="1g" --cpus="1.5" <container> updates limits on a running container without restart. The new limits take effect immediately through cgroups.

What happens to swap usage inside a container?

Docker containers use host swap by default. The --memory-swap flag controls total (RAM + swap). Without explicit limits, a container can use significant swap, causing performance degradation. For latency-sensitive apps, disable swap.

What's Next

Kubernetes Pod Lifecycle Guide
Docker Restart Policies & Health Checks
Docker Production Best Practices

Congratulations on completing this resource limits guide! Here's where to go from here:

  • Practice daily — Add resource limits to every container you run
  • Build a project — Create a monitoring dashboard for container resource usage
  • Explore related topics — cgroups v2, OOM scores, swap configuration, hugepages for databases
  • Join the community — Share your resource limit configurations and get feedback

Remember: every expert was once a beginner. Keep limiting!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro