Under the Hood #4 Understanding cgroups v2 Before Docker and Kubernetes Limit Your Containers

Learn how Linux cgroups v2 power Docker and Kubernetes by enforcing CPU, memory, I/O, and process limits. Discover how containers are isolated, why OOMKilled happens, and why cgroups are the Linux feature behind every modern container.

Under the Hood #4 Understanding cgroups v2 Before Docker and Kubernetes Limit Your Containers

In the previous article of the Under the Hood series, we followed the Linux boot process from the moment you press the power button until a Kubernetes node finally becomes Ready. Along the way, we saw how the firmware initializes hardware, GRUB loads the Linux kernel, initramfs mounts the root filesystem, and systemd starts the services that bring the operating system to life.

But once Linux has finished booting, another question naturally follows.

How does Docker stop one container from consuming all the CPU or memory on a host? How can Kubernetes safely run dozens of Pods on the same machine without letting one application affect every other workload?

Many engineers assume Docker or Kubernetes enforce these limits. In reality, they simply ask the Linux kernel to do it.

The technology responsible is control groups, better known as cgroups.

Every modern container runtime depends on cgroups. Whenever you define CPU or memory limits in a Kubernetes manifest, start a Docker container with resource constraints, or investigate an OOMKilled Pod, you're interacting with cgroups even if you never realize it.

In this article, we'll explore what cgroups are, why cgroups v2 replaced the original implementation, and how they quietly power every container running on modern Linux systems.

The Common Misconception

Ask someone what limits the resources available to a Docker container, and the answer is usually "Docker." Ask the same question about Kubernetes, and many people will say "Kubernetes resource limits."

Both answers are only partially correct.

Docker doesn't directly limit memory, CPU, or disk usage. Kubernetes doesn't enforce those limits either. They simply tell the Linux kernel what limits should be applied, and the kernel uses cgroups to enforce them.

The flow looks like this:

Docker / Kubernetes
        │
        ▼
containerd
        │
        ▼
Linux Kernel
        │
        ▼
cgroups
        │
        ▼
CPU • Memory • I/O • PIDs

This distinction is important because containers aren't a special type of operating system. From Linux's perspective, they're just ordinary processes running with additional isolation and resource controls.

Namespaces isolate workloads from one another. cgroups decide how many resources each workload is allowed to use.

Why Linux Needed cgroups

Before cgroups existed, Linux had no reliable way to divide hardware resources between applications.

Imagine a server running an NGINX web server, PostgreSQL, Redis, and a Jenkins build job. If Jenkins suddenly started compiling hundreds of projects simultaneously, it could consume most of the available CPU and memory. Every other application on the server would slow down, even though they had done nothing wrong.

Linux could schedule processes fairly, but it couldn't reserve resources for groups of related processes or prevent one workload from overwhelming another.

As workloads became larger and virtualization became more common, this limitation became increasingly problematic. The kernel needed a way to treat related processes as a single unit and enforce limits on the entire group rather than on individual processes.

That's exactly what cgroups introduced.

What Are cgroups?

A control group is exactly what its name suggests: a group of processes managed together.

Instead of monitoring every process independently, Linux places related processes into a cgroup and applies resource policies to the entire group. Those policies determine how much CPU time, memory, disk I/O, and even how many processes that group can use.

You can think of it like departments inside a company. Without departments, every employee competes for the same meeting rooms, equipment, and office space. With departments, each team receives its own allocation, preventing one group from consuming everything.

Linux applies the same idea to processes.

Linux
   │
   ▼
Control Group
   ├── Process A
   ├── Process B
   ├── Process C
   └── Process D

Every process inside the group follows the same resource limits.

This simple idea is what makes containers practical.

Why cgroups v2 Exists

The original implementation, now called cgroups v1, introduced separate hierarchies for different resource controllers. CPU had its own hierarchy, memory had another, and disk I/O had yet another.

Although flexible, this design became increasingly difficult to manage because the same process could belong to multiple independent hierarchies.

cgroups v2 simplified the model by introducing a single unified hierarchy. Instead of managing several unrelated trees, every controller now works within one consistent structure.

cgroups v1

CPU
Memory
I/O
PIDs

Separate hierarchies

──────────────

cgroups v2

One Unified Hierarchy

CPU
Memory
I/O
PIDs

The unified design makes delegation, administration, and resource accounting much simpler, which is why modern Linux distributions now enable cgroups v2 by default.

The Controllers That Matter Most

Although cgroups supports multiple controllers, four are especially important for DevOps engineers.

The CPU controller determines how processor time is shared between workloads. Instead of allowing one container to monopolize every CPU core, Linux schedules CPU time according to the limits defined by Docker or Kubernetes.

The memory controller limits RAM usage. When an application exceeds its assigned memory, Linux first attempts to reclaim cached memory. If memory pressure continues and reclaim isn't enough, the kernel's Out-Of-Memory (OOM) Killer may terminate the process.

The I/O controller manages disk bandwidth. This prevents backup jobs, databases, or logging workloads from overwhelming storage devices and degrading the performance of other applications sharing the same disks.

Finally, the PIDs controller limits the number of processes a workload can create. This protects systems from runaway applications or fork bombs that continuously spawn new processes until the operating system becomes unusable.

Together, these controllers ensure that workloads remain isolated even while sharing the same Linux host.

Exploring cgroups on a Linux System

Unlike many Linux features, cgroups aren't hidden deep inside the kernel. Their configuration is exposed through a virtual filesystem.

On most modern distributions, you'll find it here:

ls /sys/fs/cgroup

Inside this directory are files representing the active controllers and their limits, including values such as:

  • cpu.max
  • memory.max
  • memory.current
  • io.max
  • pids.max

These files aren't created by Docker or Kubernetes. They're provided directly by the Linux kernel, and container runtimes simply update them when creating new workloads.

How Docker Uses cgroups

When you execute:

docker run nginx

Docker doesn't directly enforce resource limits. Instead, it communicates with the container runtime, which asks the Linux kernel to create a new cgroup and apply the requested limits before the container process begins running.

The sequence looks like this:

docker run
      │
      ▼
containerd
      │
      ▼
Linux Kernel
      │
      ▼
Create cgroup
      │
      ▼
Apply Resource Limits
      │
      ▼
Start Container

From Linux's perspective, the container is simply another process placed inside a specially configured control group.

How Kubernetes Uses cgroups

Kubernetes follows exactly the same approach.

When you define resource limits like this:

resources:
  requests:
    memory: 512Mi
  limits:
    memory: 1Gi

the kubelet passes those values to the container runtime, which configures the appropriate cgroup before launching the container.

Pod
 │
 ▼
containerd
 │
 ▼
Linux cgroup
 │
 ▼
Memory Controller
CPU Controller
I/O Controller

Kubernetes doesn't continuously monitor or enforce these limits itself. Once the Pod starts, the Linux kernel becomes responsible for enforcing them.

Why OOMKilled Happens

One of the most common production issues Kubernetes engineers encounter is an OOMKilled Pod.

Many assume this simply means the application used too much memory. The reality is slightly more nuanced.

When a process approaches its configured memory limit, Linux first attempts to free cached pages. If memory pressure remains high, reclaim continues. Only when no reasonable recovery is possible does the kernel invoke the OOM Killer, which selects a process to terminate so the system can continue operating.

That process exits with code 137, and Kubernetes reports the container as OOMKilled.

Understanding this sequence explains why increasing memory limits isn't always the correct solution. Sometimes the application has a memory leak, sometimes requests and limits are poorly configured, and sometimes the node itself is already under heavy memory pressure.

A Real Production Investigation

Imagine a Kubernetes Pod repeatedly entering the OOMKilled state.

The obvious response might be to double the memory limit.

An experienced engineer investigates first.

They compare the current memory usage against the configured limit, inspect kernel logs with dmesg, review Pod events using kubectl describe pod, and determine whether the application genuinely requires more memory or whether a bug is causing uncontrolled allocation.

In many cases, the real issue isn't insufficient memory at all. It's a workload consuming resources in an unexpected way, and cgroups are simply enforcing the limits that were configured.

Common Mistakes

One of the most common misconceptions is believing Docker or Kubernetes directly enforce resource limits. In reality, both depend entirely on Linux cgroups.

Another mistake is assuming every OOMKilled event means more memory should be allocated. Sometimes increasing limits simply hides the underlying problem instead of solving it.

Finally, many engineers never inspect cgroup metrics directly. Looking at files such as memory.current or memory.max often provides far more insight than container-level monitoring dashboards alone.

Key Takeaways

cgroups are a Linux kernel feature that allows related processes to share resource limits. Docker and Kubernetes rely on them for CPU, memory, disk I/O, and process management. Modern Linux systems use cgroups v2 because its unified hierarchy is simpler and more consistent than the original implementation.

Whenever you define resource limits for a container, you're ultimately configuring Linux cgroups even if Docker or Kubernetes hides that complexity behind a much friendlier interface.

Conclusion

Containers wouldn't exist in their current form without cgroups.

Every time Docker launches a container, or Kubernetes schedules a Pod, the Linux kernel quietly creates a control group that governs how much CPU, memory, storage bandwidth, and how many processes that workload can consume.

Most engineers only become aware of cgroups after seeing an OOMKilled container or investigating CPU throttling. But understanding how cgroups work transforms those incidents from mysterious failures into predictable Linux behaviour.

The next time you define a memory limit in Kubernetes, remember that Kubernetes isn't enforcing it.

Linux is.

Next in the Under the Hood Series

Under the Hood #5: How Linux Networking Really Works Before Docker and Kubernetes

We'll follow a network packet from the moment it enters a network interface card (NIC) until it reaches an application, exploring routing tables, virtual Ethernet pairs, bridges, iptables, conntrack, and how Docker and Kubernetes build modern container networking on top of Linux.