Photo by Shubham Dhage on Unsplash
Resource Limits and Cgroups: How Container Isolation Actually Works
When you set memory limits on a container or throttle CPU usage in Kubernetes, you’re working with control groups—cgroups—a Linux kernel feature that partitions, prioritizes, and accounts for system resources across process hierarchies. Containers feel isolated because cgroups enforce boundaries at the kernel level, not because Docker or containerd creates true virtualization.
What Cgroups Actually Do
A control group is a kernel mechanism that organizes processes into hierarchical groups and applies resource constraints to each group. Unlike traditional per-process resource limits (ulimit), cgroups manage collections of processes and their children as a unit. When you launch a container, the runtime creates a new cgroup, places the container’s init process inside it, and configures controllers for memory, CPU, block I/O, and network traffic.
Cgroups v1, the original implementation, organized controllers into separate hierarchies—one for memory, another for CPU, and so on. Cgroups v2, now the default in most distributions, unifies these into a single hierarchy, simplifying management and improving consistency. The v2 design also introduces pressure stall information (PSI), which exposes when a cgroup is starved for resources, enabling smarter scheduling and alerting.
Memory Limits and the OOM Killer
When you set a memory limit, the memory controller tracks every page allocated by processes in the cgroup. If the group exceeds its limit, the kernel tries to reclaim memory by evicting clean pages and swapping. If reclaim fails and the cgroup hits the hard limit, the out-of-memory killer activates—but only within that cgroup. The OOM killer scores processes by memory usage and oom_score_adj, then kills the highest-scoring process to free memory.
This scoped behavior is what prevents one container from crashing the host. A memory-hungry container might kill its own processes, but it won’t trigger a system-wide OOM event that affects other workloads. In Kubernetes, setting memory requests and limits maps directly to cgroup memory.min, memory.low, and memory.max parameters, giving the scheduler and kubelet the information they need to place and evict pods.
CPU Shares, Quotas, and Throttling
CPU control works through two mechanisms: shares and quotas. CPU shares set relative weight—if one cgroup has 1024 shares and another has 512, the first gets roughly twice as much CPU time when both are competing. Shares only matter under contention; an idle system lets a single cgroup use all available cycles.
CPU quotas impose hard limits. A quota of 50,000 microseconds per 100,000-microsecond period means the cgroup can use 50% of one core. If processes in the cgroup exhaust their quota before the period ends, they’re throttled until the next period. This throttling is visible in container metrics as cpu.cfs_throttled_us, and excessive throttling often indicates misconfigured limits or bursty workloads that need larger periods or no quota at all.
Kubernetes CPU requests map to shares (via cpu.weight in v2), while limits map to quotas. Setting a limit without understanding burst behavior can lead to artificial latency spikes, even when the host has spare capacity.
I/O and Device Limits
The block I/O controller (blkio in v1, io in v2) manages read and write bandwidth and IOPS on a per-device basis. You can set absolute limits, weighted priorities, or both. This prevents one container from saturating disk I/O and starving others, especially in multi-tenant environments or when mixing latency-sensitive and batch workloads on the same host.
Device limits restrict access to specific devices using a whitelist (devices.allow) or blacklist (devices.deny). Combined with cgroups, this is part of how containers prevent a process from accessing raw block devices or hardware it shouldn’t touch, even though it shares the same kernel as the host.
Why Cgroups Matter Beyond Containers
Cgroups predate Docker. Systemd uses them to manage every service, applying resource limits and accounting to system daemons. Cloud providers use cgroups to enforce VM-level CPU and memory quotas even in environments that layer containers atop virtual machines. Batch schedulers and job queues rely on cgroups to isolate workloads, track resource usage, and enforce fair sharing.
Understanding cgroups clarifies why container performance behaves the way it does—why a container might be throttled despite low host CPU usage, why memory limits trigger OOM kills before swap is full, and why I/O-heavy workloads need separate tuning. The abstraction that runtimes and orchestrators provide is useful, but the kernel’s resource control is where the actual isolation happens.