Skip to main content

Docker Resource Constraints — CPU, Memory, and cgroups

Configure CPU and memory limits on Docker containers using cgroups to prevent resource exhaustion and ensure stable multi-container host performance.

44 Terms

Overview and What You Will Learn

By default, a Docker container can use all the CPU and memory on the host. This means one badly behaving container — a memory leak, an infinite loop, a sudden traffic spike — can consume every available resource and crash every other container running on the same host. In production, this is unacceptable.

In this guide you will learn how to set CPU and memory limits on Docker containers, what happens when those limits are exceeded (the answer is different for CPU and memory), how to monitor resource usage in real time, and how to set limits in Docker Compose. You will also understand the Linux cgroup mechanism that makes all of this possible.

Why This Matters in Production

At CRED, dozens of microservices run on shared EC2 instances. Without resource limits, a payment processing container with a memory leak could consume all available memory on the host, killing the database container, the cache container, and every other service on that machine. Resource limits are the insurance policy that prevents one container's failure from becoming every container's failure.

Core Principles

Docker uses Linux cgroups (control groups) to enforce resource limits. Cgroups are a kernel feature that limits the resources a group of processes can use. Docker creates a cgroup for each container and applies your limits to it.

◈ DIAGRAM
+------------------------------------------+
| Linux Host |
| 16 CPU cores, 32GB RAM |
| |
| +------------------+ |
| | Container A | <- CPU limit: 2 |
| | cgroup enforced | Memory: 512MB |
| +------------------+ |
| |
| +------------------+ |
| | Container B | <- CPU limit: 4 |
| | cgroup enforced | Memory: 2GB |
| +------------------+ |
| |
| +------------------+ |
| | Container C | <- No limits set |
| | NO cgroup limits | DANGEROUS |
| | Can use all 16 | |
| | CPUs and 32GB | |
| +------------------+ |
+------------------------------------------+

The most important thing to understand: CPU exceeded = throttled. Memory exceeded = killed.

TEXT
CPU limit exceeded:
Container tries to use more than its limit
Kernel throttles it — slows down, stays running
Other containers are not affected
Application becomes slow but does not crash
Memory limit exceeded:
Container tries to allocate more than its limit
Kernel OOMKills the container process immediately
Exit code 137
Container stops (and restarts if restart policy is set)

Detailed Step-by-Step Practical Lab

Milestone 1: Setting Memory Limits
Bash
docker run -d \
--name payment-api \
--memory=512m \
registry.razorpay.in/payment-api:v3.1.0
# Memory values: b (bytes), k (kilobytes), m (megabytes), g (gigabytes)
# --memory=512m = 512 megabytes
# --memory=2g = 2 gigabytes
# Verify the limit was applied
docker inspect --format '{{.HostConfig.Memory}}' payment-api
# 536870912 = 512 * 1024 * 1024 bytes = 512MB
# --memory-swap controls total memory + swap
# By default: --memory-swap equals --memory (no swap allowed)
# To allow swap: set --memory-swap higher than --memory
docker run -d \
--name payment-api \
--memory=512m \
--memory-swap=1g \
registry.razorpay.in/payment-api:v3.1.0
# Container can use 512MB RAM + 512MB swap (1GB total - 512MB RAM)
# To disable swap entirely for this container:
docker run -d \
--name payment-api \
--memory=512m \
--memory-swap=512m \
registry.razorpay.in/payment-api:v3.1.0
# --memory-swap equals --memory = no swap allowed
# Soft memory limit (reservation) — Docker can use this for scheduling hints
docker run -d \
--name payment-api \
--memory=512m \
--memory-reservation=256m \
registry.razorpay.in/payment-api:v3.1.0
# Guarantees 256MB minimum, allows bursting to 512MB
# Useful when running on Kubernetes-like schedulers
Milestone 2: What Happens When Memory Limit Is Exceeded
Bash
# Simulate an OOMKill
docker run -d \
--name oom-test \
--memory=50m \
python:3.11-slim \
python3 -c "
x = []
while True:
x.append(' ' * 10000000) # allocate 10MB chunks
print(f'Allocated {len(x) * 10}MB')
"
# Watch it get killed
docker logs -f oom-test
# Allocated 10MB
# Allocated 20MB
# Allocated 30MB
# Allocated 40MB
# (killed — no more output)
# Check exit code
docker inspect --format '{{.State.ExitCode}} OOMKilled: {{.State.OOMKilled}}' oom-test
# 137 OOMKilled: true
# The kernel logs this in dmesg
sudo dmesg | grep -i "killed process"
# [1234567.890] Killed process 12345 (python3) total-vm:60000kB,
# anon-rss:55000kB, file-rss:1000kB, shmem-rss:0kB
Milestone 3: Setting CPU Limits

CPU limits work differently from memory limits. There are two main ways to limit CPU:

Bash
# Method 1: --cpus — fractional CPU cores (recommended)
docker run -d \
--name api \
--cpus=1.5 \
registry.swiggy.in/order-api:v2.0.0
# Container can use at most 1.5 CPU cores worth of processing
# On a 4-core host: this container gets at most 37.5% of total CPU
# --cpus=0.5 = half a CPU core
# --cpus=1 = one full CPU core
# --cpus=2.5 = two and a half CPU cores
# Method 2: --cpu-shares — relative weight (soft limit)
docker run -d --name high-priority --cpu-shares=1024 high-priority-service
docker run -d --name low-priority --cpu-shares=512 low-priority-service
# When both are competing for CPU:
# high-priority gets 2x the CPU time of low-priority
# When only one is running, it can use 100% of CPU
# Default cpu-shares is 1024
# Method 3: Pin to specific CPUs
docker run -d \
--name cpu-pinned \
--cpuset-cpus="0,1" \
registry.zerodha.in/trading-engine:v4.1.0
# This container ONLY runs on CPU cores 0 and 1
# Useful for performance-sensitive workloads that benefit from CPU affinity
# And for preventing noisy neighbours from sharing CPU cache
# Verify CPU limits applied
docker inspect --format '{{.HostConfig.NanoCpus}}' api
# 1500000000 = 1.5 * 1000000000 nanocpus = 1.5 CPUs
docker inspect --format '{{.HostConfig.CpusetCpus}}' cpu-pinned
# 0,1
Milestone 4: Monitoring Resource Usage
Bash
# Live monitoring
docker stats
# Sample output:
# CONTAINER ID NAME CPU % MEM USAGE/LIMIT MEM % NET I/O BLOCK I/O
# a84f9c2b1d3e payment-api 2.5% 128MiB/512MiB 25.0% 2MB/1MB 0B/0B
# b72c8a9f4e1d order-api 45.2% 420MiB/512MiB 82.0% 10MB/5MB 1MB/0B
# c91d8b3f2a5e postgres 0.8% 256MiB/1GiB 25.0% 100B/50B 50MB/5MB
# Reading the table:
# order-api at 45.2% CPU — high but within the 1.5 CPU limit
# order-api at 82% memory — getting close to the 512MB limit, investigate!
# postgres at 0.8% CPU — healthy, mostly idle
# One-shot snapshot — useful in scripts
docker stats --no-stream --format \
"{{.Name}}: CPU={{.CPUPerc}} MEM={{.MemUsage}} ({{.MemPerc}})"
# payment-api: CPU=2.5% MEM=128MiB / 512MiB (25.0%)
# order-api: CPU=45.2% MEM=420MiB / 512MiB (82.0%)
# Watch for containers approaching their limits
watch -n 5 'docker stats --no-stream'
# Update limits on a running container without stopping it
docker update --memory 1g payment-api
docker update --cpus 2 payment-api
# This changes the cgroup limits live — no restart needed
# Useful when you need to give a struggling container more resources immediately
Milestone 5: Resource Limits in Docker Compose

Setting limits per-container with docker run flags does not scale. In Docker Compose, resource limits go in the deploy.resources section:

YAML
# docker-compose.yml
version: "3.8"
services:
payment-api:
image: registry.razorpay.in/payment-api:v3.1.0
ports:
- "8080:8080"
deploy:
resources:
limits:
cpus: "1.5" # Hard ceiling — throttled if exceeded
memory: 512M # Hard ceiling — OOMKilled if exceeded
reservations:
cpus: "0.5" # Soft guarantee — scheduler reserves this
memory: 256M # Soft guarantee — scheduler reserves this
order-api:
image: registry.swiggy.in/order-api:v2.0.0
deploy:
resources:
limits:
cpus: "2"
memory: 1G
reservations:
cpus: "0.5"
memory: 512M
postgres:
image: postgres:15
volumes:
- postgres-data:/var/lib/postgresql/data
deploy:
resources:
limits:
cpus: "2"
memory: 2G
reservations:
cpus: "0.5"
memory: 1G
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db-password
volumes:
postgres-data:
Bash
# Run compose with resource limits applied
docker compose up -d
# Verify limits were applied
docker stats --no-stream
Milestone 6: Choosing the Right Limits

The most common question is: how do I know what limits to set? The answer is always: measure first, then set.

Bash
# Step 1: Run the container without limits for a period
docker run -d --name measure-api registry.razorpay.in/payment-api:v3.1.0
# Step 2: Generate realistic load (your normal traffic pattern)
# Use a load testing tool: k6, wrk, Apache Bench, Locust
# Step 3: Monitor resource usage under load
docker stats --no-stream measure-api
# NAME CPU % MEM USAGE MEM %
# measure-api 35.2% 380MiB (no limit)
# Step 4: Set limits based on observed peak + safety margin
# CPU: set limit to ~2x observed peak (allows burst, prevents monopoly)
# Peak observed: 35% of 1 CPU = 0.35 CPUs
# Set limit to: 0.75 CPUs (2x with some headroom)
# Memory: set limit to observed peak + 50% safety margin
# Peak observed: 380MiB
# Set limit to: 570MiB, round up to 600MiB
docker run -d \
--name payment-api \
--cpus=0.75 \
--memory=600m \
registry.razorpay.in/payment-api:v3.1.0

Connection to Kubernetes

If you are learning Docker as a path to Kubernetes, resource limits are where the two platforms align perfectly:

◈ DIAGRAM
Docker Kubernetes
--memory=512m -> resources.limits.memory: 512Mi
--memory-reservation= -> resources.requests.memory:
--cpus=1.5 -> resources.limits.cpu: 1500m
--cpu-shares (soft) -> resources.requests.cpu:
OOMKilled (exit 137) -> OOMKilled pod status (same kernel mechanism)
CPU throttled -> CPU throttled (same cgroup mechanism)

The exact same Linux cgroup mechanism is used. Understanding Docker resource limits means understanding Kubernetes resource limits.

Common Mistakes

Mistake Consequence Fix
No memory limit on any container One leaking container crashes the host Set limits on every production container
Setting memory limit too low Random OOMKills that look like app crashes Measure peak usage + 50% margin
Not setting --memory-swap Container can use unlimited swap, causing I/O thrash Set --memory-swap equal to --memory to disable swap
Using --cpu-shares alone Only affects CPU-contention periods, not a hard limit Use --cpus for actual CPU ceiling enforcement
Setting limits in compose but using docker compose up without -d On some Docker versions, deploy.resources is ignored in non-swarm mode Verify with docker stats that limits are applied

Troubleshooting Reference

Symptom Likely Cause Diagnostic Command Fix
Container exits with code 137 OOMKilled docker inspect --format '{{.State.OOMKilled}}' Increase --memory limit
Container slow but not crashing CPU throttled docker stats — check CPU % vs limit Increase --cpus limit
Container crashes and restarts repeatedly Memory leak hitting limit docker stats over time — memory growing Fix app memory leak or increase limit temporarily
Host runs out of memory No limits on containers docker stats --no-stream Set limits on all containers immediately
Tip

Always set both limits and reservations in Docker Compose. Limits prevent a container from consuming too much. Reservations tell the scheduler to guarantee a minimum — useful when containers compete for resources and you want to ensure critical services always have enough.

Remember

The Linux OOM killer uses exit code 137 — the same exit code as docker kill (SIGKILL). Always check docker inspect --format '{{.State.OOMKilled}}' to confirm an OOMKill versus a manual kill before spending time investigating application code.

Common Mistake

Setting memory limits without load testing first. Engineers often set limits based on gut feeling — 512MB for an API sounds reasonable — but the application might use 800MB under real load. The result is random OOMKills in production that look like application bugs. Always measure under realistic load before setting limits.

Security

On shared infrastructure, resource limits are a security control as well as an operational one. A container without limits can perform a denial-of-service attack on other containers by consuming all CPU or memory. Even for trusted internal services, always set limits — a bug causing a memory leak in a trusted container is just as dangerous as a malicious container with no limits.

Resources

Explore More in Docker Fundamentals and Container Lifecycle

All 3 Topics

Frequently Asked Questions

Is Docker Resource Constraints — CPU, Memory, and cgroups free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Docker Resource Constraints — CPU, Memory, and cgroups topic cover?

Configure CPU and memory limits on Docker containers using cgroups to prevent resource exhaustion and ensure stable multi-container host performance.