Skip to main content

Resource Limits and Cgroups

By default, a container can use all of the host's memory and CPU. If one container has a memory leak or gets hit with heavy traffic, it can starve other containers and crash the entire server. Resource limits prevent this by capping what each container can consume.

Production Rule

Never run a container in production without a memory limit. A single runaway container can take down everything on the host.

How Resource Limits Work

Docker uses Linux cgroups (control groups) to enforce resource limits. When a container exceeds its limits, the kernel steps in:

flowchart TD
A["Container Process"] --> B{"Memory usage"}
B -->|"Below limit"| C["Normal operation"]
B -->|"At limit"| D["OOM Killer invoked"]
D --> E["Process killed (exit code 137)"]

A --> F{"CPU usage"}
F -->|"Below limit"| G["Normal operation"]
F -->|"At limit"| H["CPU throttled (slowed down)"]

style C fill:#e8f5e9,stroke:#2e7d32
style G fill:#e8f5e9,stroke:#2e7d32
style E fill:#ffebee,stroke:#c62828
style H fill:#fff3e0,stroke:#ef6c00

The key difference: memory limits are hard (exceed → killed), CPU limits are soft (exceed → throttled/slowed).

Memory Limits

FlagWhat It DoesExample
--memory / -mHard memory limit. Container is killed (OOM) if it exceeds this--memory="512m"
--memory-swapTotal RAM + swap limit. Set to same as --memory to disable swap--memory-swap="512m"

CLI Example

docker run -d --name api --memory="512m" my-api:1.0.0

Compose Example

services:
api:
image: my-api:1.0.0
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M

What Happens When the Limit Is Reached?

The Linux kernel's OOM Killer terminates the container's main process. The container exits with code 137. You can check if this happened:

docker inspect -f '{{.State.OOMKilled}}' my-container
# true = killed by OOM

CPU Limits

FlagWhat It DoesExample
--cpusMaximum CPU cores the container can use (fractional)--cpus="1.5" (1.5 cores)
--cpuset-cpusPin the container to specific CPU cores--cpuset-cpus="0,1"

CLI Example

# Limit to half a CPU core
docker run -d --name worker --cpus="0.5" my-worker:1.0.0

Unlike memory limits, hitting a CPU limit does not kill the container. Instead, the process is throttled -- it runs slower. This can cause latency spikes that look like application bugs.

Process (PID) Limits

A container can spawn unlimited processes by default. A fork bomb or runaway process spawner can exhaust the host's PID space:

docker run -d --name api --pids-limit=256 my-api:1.0.0

This limits the container to 256 concurrent processes.

Monitoring Resource Usage

Live Stats

docker stats

Output:

CONTAINER ID   NAME    CPU %     MEM USAGE / LIMIT     MEM %     PIDS
a1b2c3d4e5f6 api 0.05% 12.5MiB / 512MiB 2.44% 15
f6e5d4c3b2a1 worker 45.30% 480MiB / 512MiB 93.75% 42

Watch for: memory usage consistently above 80% of the limit (risk of OOM), or CPU at 100% of its allocation (throttling).

Inspect Current Limits

# Memory limit (in bytes)
docker inspect -f '{{.HostConfig.Memory}}' my-container

# CPU limit (in nanocpus: 1 CPU = 1000000000)
docker inspect -f '{{.HostConfig.NanoCpus}}' my-container

# OOM killed?
docker inspect -f '{{.State.OOMKilled}}' my-container

Choosing Initial Limits

Service TypeMemory Starting PointCPU Starting Point
Node.js web server256m - 512mUsually leave unlimited
Python web server256m - 512mUsually leave unlimited
Go web server64m - 256mUsually leave unlimited
PostgreSQL512m - 2g1.0 - 2.0
Redis128m - 512mUsually leave unlimited
Worker / batch jobTask-dependentTask-dependent

Tuning Workflow

  1. Observe baseline usage under normal traffic using docker stats
  2. Set initial limit at ~2x the observed baseline
  3. Monitor for OOM kills or throttling after deploying
  4. Adjust one variable at a time based on data
  5. Document the final values and rationale

Complete Example

# Run with memory, CPU, and PID limits
docker run -d \
--name api \
--memory=512m \
--cpus=1.0 \
--pids-limit=256 \
--restart unless-stopped \
my-api:1.0.0

# Monitor usage
docker stats api

# Check for OOM after an incident
docker inspect -f '{{.State.OOMKilled}}' api

Common Mistakes

MistakeConsequenceFix
No memory limit in productionOne container can crash the entire hostAlways set --memory
Limit set too lowConstant OOM kills under normal trafficObserve baseline first, set 2x headroom
Same limits for all servicesWorkers need different profiles than APIsTune per service type
Ignoring OOM eventsMemory leak goes undetectedMonitor OOM flag and alert on kills
Very tight CPU limitsLatency spikes that look like application bugsMonitor latency after setting CPU limits

Key Takeaways

  • Memory limits are mandatory for production -- without them, one container can crash your server.
  • Memory exceeding the limit → process killed (OOM). CPU exceeding the limit → process slowed (throttled).
  • Use docker stats to observe baseline usage, then set limits with headroom.
  • Check OOMKilled flag after unexplained container exits.
  • Tune limits iteratively based on real data, not guesses.

What's Next