Pause Container

The pause container is the per-Pod infrastructure container that holds open the Linux namespaces — primarily network and IPC, optionally PID — that the Pod’s user containers all join. It is the anchor that gives the Pod its identity as a network endpoint and its property of shared interfaces between sibling containers. The reference implementation is in kubernetes/kubernetes/build/pause (source): a static C binary of a few hundred kilobytes (build-dependent — see the size caveat below) that executes a single function — pause() blocking on signals, with a SIGCHLD handler that wait()s on reaped zombies. The pause container is therefore both the smallest binary in the typical Kubernetes node and one of the most load-bearing: every Pod has exactly one, and every other container in the Pod is configured to join its namespaces rather than create new ones (Lewis 2017 — The Almighty Pause Container). The “Pod” abstraction in Kubernetes is, mechanically, “a pause container plus a set of user containers that have joined its namespaces.” Understanding the pause container is what makes the Pod model click as a Linux-kernel construct rather than a Kubernetes-doc construct.

Mental Model

flowchart TB
    KUBELET["kubelet on Node X"] -->|"RunPodSandbox (CRI)"| RUNTIME["containerd / CRI-O"]
    RUNTIME -->|"runc create pause"| PAUSE["pause container<br/>(PID, NET, IPC, UTS namespaces)"]

    PAUSE -.->|owns| NET[(Network namespace<br/>veth, lo, eth0)]
    PAUSE -.->|owns| IPC[(IPC namespace<br/>shm, msgq)]
    PAUSE -.->|owns| UTS[(UTS namespace<br/>hostname)]
    PAUSE -.->|owns| PID[(PID namespace<br/>optional, off by default)]

    RUNTIME -->|"runc create app, join pause's namespaces"| APP1["app container 1<br/>nginx"]
    RUNTIME -->|"runc create sidecar, join pause's namespaces"| APP2["app container 2<br/>fluent-bit sidecar"]

    APP1 -.->|setns into| NET
    APP1 -.->|setns into| IPC
    APP1 -.->|setns into| UTS
    APP2 -.->|setns into| NET
    APP2 -.->|setns into| IPC
    APP2 -.->|setns into| UTS

    CNI["CNI plugin (Calico/Cilium/AWS VPC)"] -->|"attaches veth into"| NET

Caption. The pause container is the namespace landlord of the Pod. It exists not to do anything visible, but to hold the kernel resources (namespace inodes) that other containers join. The diamond-arrow setns() is the actual mechanism: when runc sees linux.namespaces[].path set to /proc/$pause_pid/ns/net, it joins that namespace instead of creating a new one. The CNI plugin attaches the Pod’s eth0 to the pause container’s netns; when the user containers join it, they see that eth0 automatically. The insight to extract: the user containers don’t have their own network namespaces — they share the pause container’s. Killing all user containers leaves the namespace alive (because the pause container is still in it); killing the pause container tears down the Pod’s network identity.

Mechanical Walk-through

Why a Separate Container at All — Three Reasons

If the goal is just “hold open namespaces”, you might imagine kubelet could create namespaces directly and never run any “pause” process. Three reasons that doesn’t work:

  1. Namespaces are reference-counted by processes, not by kubelet. A Linux namespace exists as long as at least one process is a member of it (the kernel deletes the namespace inode when its refcount hits zero). Without a long-lived holder process, the moment all user containers exit (e.g., during a crashloop), the namespace would vanish, the Pod IP would be released, and the next user-container restart would land in a fresh namespace with a different IP. The CNI binding, iptables rules, and routing entries would all need re-creation. The pause container’s only job is to never exit so that the namespace inode never gets refcount-zero’d between user-container restarts.

  2. Init/reaper duties. When PID-namespace sharing is enabled (off by default; opt-in via shareProcessNamespace: true), some process must be PID 1 inside that namespace. PID 1 has a unique kernel role: it reaps wait()able zombies whose original parent has died (reparent_to_init), and it receives signals like SIGTERM as the lifecycle dispatcher. Most user binaries (nginx, JVMs, Python apps) are not written to be PID 1 — they don’t install reaper handlers, they don’t catch SIGTERM cleanly. The pause container, by contrast, is exactly a properly-behaved PID 1: a pause() blocking loop with a SIGCHLD handler that wait()s zombies. Even when PID namespace is not shared, the kernel’s PR_SET_CHILD_SUBREAPER mechanism gives the pause container reaper status for orphans inside its sub-tree.

  3. Lifecycle coherence. The orchestrator wants “the Pod” to be a unit with one identity, one lifecycle, one place to attach networking, one place to associate cgroup parents. Having a designated infrastructure container makes “the Pod exists” equivalent to “the pause container exists.” kubelet’s reconciliation logic, crictl ps’s pod-sandbox listing, and the CRI RunPodSandbox / StopPodSandbox calls all key off the pause container’s existence.

This design choice traces directly back to Borg’s “alloc” concept (Verma et al. 2015, Burns et al. 2016) — a Borg alloc was a resource reservation on a machine that one or more “tasks” shared, much as a Pod is a resource reservation that one or more containers share. The pause container is the K8s implementation of “the alloc exists even when no task is running in it yet.”

The Source — A Few Dozen Lines of C

The actual implementation in kubernetes/kubernetes/build/pause/linux/pause.c is fewer than 100 lines of C. The shape:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
 
static void sigdown(int signo) {
    psignal(signo, "Shutting down, got signal");
    exit(0);
}
 
static void sigreap(int signo) {
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}
 
int main(int argc, char **argv) {
    // (real source also parses a -v flag that prints the version and exits)
 
    // The binary warns — but does not fail — if it is not PID 1, because
    // pause "sees use outside of infra containers" too:
    if (getpid() != 1)
        fprintf(stderr, "Warning: pause should be the first process\n");
 
    // Install handlers for graceful shutdown
    if (sigaction(SIGINT,  &(struct sigaction){.sa_handler = sigdown}, NULL) < 0) return 1;
    if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0) return 2;
    // Install zombie reaper
    if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
                                                .sa_flags = SA_NOCLDSTOP}, NULL) < 0) return 3;
 
    for (;;) pause();      // block until a signal arrives
    return 42;             // unreachable; the real source returns 42 here
}

This matches the upstream kubernetes/kubernetes/build/pause/linux/pause.c (source): a -v version flag, the getpid() != 1 warning (a warning rather than an error because the binary is occasionally used outside an infra-container context), the two signal handlers, the for (;;) pause(); loop, and a final return 42 that can never actually run. Compiled statically with -static and stripped, the resulting pause binary is on the order of a few hundred kilobytes on x86_64 — most of that being the static C runtime. The Go-language alternative implementation that lived alongside the C one (pause.go) is several megabytes by comparison, which is why production images standardized on the C version. The binary is shipped in an image conventionally tagged registry.k8s.io/pause:3.X; as of the Kubernetes release in development in 2026 (heading toward 1.36) the kubeadm PauseVersion constant is 3.10.2 (kubeadm constants.go), with 3.9 and 3.10 being the tags seen across the 1.27–1.31 era.

What kubelet Does With It

When kubelet decides to create a Pod, the CRI dance is:

  1. RunPodSandbox(podConfig) — kubelet asks the CRI runtime to create the sandbox. The runtime pulls the pause image, creates the namespaces (PID, NET, IPC, UTS), invokes the CNI plugin to attach networking to the netns, sets cgroup parents, and runc runs the pause binary inside the namespaces.
  2. CNI plugin runs against the pause container’s netns inode (/proc/$pause_pid/ns/net): creates a veth pair, moves one end into the netns, configures the IP, sets the default route. Now the Pod has an IP.
  3. For each user container in the Pod spec: CreateContainer(sandboxID, config). The runtime builds an OCI bundle whose config.json lists the same NET/IPC/UTS namespaces with path: set to the pause container’s namespace inodes. runc create joins them via setns(2).
  4. StartContainer(containerID) for each: runc start. The user process is now inside the pause container’s network/IPC/UTS world but in its own mount namespace (so its rootfs is private) and, by default, its own PID namespace.

The reverse on shutdown: kubelet calls StopContainer for each user container, then StopPodSandbox, which kills the pause container — at which point the namespace refcounts hit zero and the Pod’s network identity is released back to the kernel.

Why the Pause Container Doesn’t Need PID 1 Inside Each User Container

When shareProcessNamespace: false (the default), each user container gets its own PID namespace. Inside that namespace, the container’s own entry process is PID 1 — so the user container’s binary takes on the reaper-and-signal-handler role for its sub-tree, not the pause container. The pause container’s reaper duties only matter for processes within its own PID namespace, which by default contains only the pause binary itself.

When shareProcessNamespace: true, all containers in the Pod share the pause container’s PID namespace. Now the pause container is PID 1 for the entire Pod. Re-parented zombies from any sibling container flow to the pause container; this is exactly the scenario for which the C sigreap handler exists.

The Pod’s --init story (the older flag) and modern restartPolicy: Always interplay with this: if a user container’s app is poorly behaved as PID 1, the workaround is either shareProcessNamespace: true (let pause reap) or injecting a proper init (tini, dumb-init) as the container’s entrypoint.

Configuration / API Surface

The pause container is not something users configure directly — it is created by the CRI runtime as part of RunPodSandbox. The visible surface is via crictl:

# All sandboxes (each is a pause container)
$ sudo crictl pods
POD ID              CREATED  STATE   NAME             NAMESPACE   ATTEMPT
4e7b2c8a3f9d1...    2m ago   Ready   nginx-585449...  default     0
8a1c2d4e5f6a7...    5m ago   Ready   coredns-668...   kube-system 0
 
# Containers (note: pause is not listed here — only user containers)
$ sudo crictl ps
CONTAINER ID    IMAGE       CREATED  STATE    NAME    POD ID
0a1b2c3d4e5f... nginx:1.25  2m ago   Running  nginx   4e7b2c8a3f9d1...
 
# But the pause container is visible in the process tree
$ ps -ef --forest | grep -A2 containerd-shim
root     10240  1234  containerd-shim-runc-v2 -namespace k8s.io -id 4e7b2c8a3f9d1 ...
root     10260 10240   \_ /pause
root     10380 10240   \_ nginx: master process nginx -g daemon off;
 
# The pause process and the user process are siblings under the same shim.
# The user process is in a PID namespace where it sees itself as PID 1;
# from the host, both are normal processes under containerd-shim.
 
# The image used:
$ sudo crictl inspectp 4e7b2c8a3f9d1... | jq '.info.config.metadata, .info.runtimeSpec.process'

The image kubelet uses for pause is configurable via the kubelet flag --pod-infra-container-image (legacy) or, more correctly today, via the CRI runtime’s configuration (containerd’s sandbox_image in /etc/containerd/config.toml, CRI-O’s pause_image in /etc/crio/crio.conf). Air-gapped clusters customarily override this to point at an internal mirror; mismatched image tags between kubelet and the runtime are a classic “pods stuck in ContainerCreating” cause.

Failure Modes

Pause container OOM (rare but catastrophic). The pause binary uses essentially no memory (a few hundred KB resident), so it is essentially never OOM-killed on its own. But if the Pod’s cgroup memory limit is set absurdly low (e.g., total Pod limit of 10 Mi for a pod that includes a real app), and the kernel OOM-killer picks the pause container as a victim because it’s the largest-RSS-share-of-limit process at the moment, the entire Pod’s namespaces collapse. All sibling containers lose their network/IPC namespace anchor and crash. Diagnose via dmesg | grep oom_kill showing the pause binary as the victim. The fix is to raise Pod limits or fix the misconfiguration.

Sandbox creation timeout. If the CRI runtime can’t pull the pause image (air-gapped cluster with no mirror, registry auth misconfigured, image tag mismatch), RunPodSandbox hangs and the Pod sits in ContainerCreating indefinitely. kubectl describe pod shows events like Failed to create pod sandbox: rpc error: code = Unknown desc = failed to get sandbox image "registry.k8s.io/pause:3.9": .... Fix: pre-pull the pause image or correct the runtime’s sandbox_image setting.

Stale netns after pause crash. If the pause container is killed (kill -9 from the host, OOM, or — historically — a runc bug), the kernel reaps the namespace once refcounts hit zero. Any user container still inside that netns is reparented to init (PID 1 on the host) and its network suddenly vanishes (the netns is gone). Symptoms: pod loses connectivity mid-run with no obvious event; user containers continue running, confused. The recovery is for kubelet to detect the sandbox failure and recreate the Pod from scratch.

Pause image version skew on CRI runtime upgrade. Kubernetes periodically bumps the recommended pause image (e.g., 3.73.83.93.103.10.2), and crucially the kubelet’s expected default and the runtime’s sandbox_image are configured independently. A real and recurring symptom of this split is documented in kubernetes#106903: after upgrading to v1.23.0 the running pause container was still 3.5 while kubeadm config images list reported 3.6, because containerd’s own sandbox_image setting governs what is actually pulled — not kubelet. When you upgrade the CRI runtime (containerd, CRI-O) but its config still references an old sandbox_image: registry.k8s.io/pause:3.6 that has been removed from the registry, sandbox creation fails for new pods. Existing pods keep running because their pause container is already in memory, so the problem surfaces only after a node reboot or pod restart.

Static pod manifest typos and the pause container. A static-pod manifest with a malformed spec produces a Pod whose user containers fail to create — but the pause container is created first, so crictl pods shows a sandbox in NotReady and crictl ps shows no user containers. See Static Pods for the broader failure category.

Uncertain

Verify: the exact on-disk size of the current C pause binary. Reason: it is not stated in any primary doc and varies by build (musl vs glibc-static, strip flags, target arch), so the “few hundred kilobytes” figure is an approximation rather than a cited number; the historical Go build was several megabytes. To resolve: du -b $(which pause) inside the registry.k8s.io/pause:3.10.2 image, or read the size reported in the build pipeline artifacts.

Alternatives and When to Choose Them

For “an infrastructure container per Pod” — no real alternative. The pause container’s role is structurally required by the Pod abstraction. The Kubernetes-internal alternatives have been refinements rather than replacements:

  • Different binaries for pause. The pause container was “rewritten entirely in C” at pause-image version 3.0 (build/pause CHANGELOG, PR #23009); the SIGCHLD zombie-reaping handler and the -v version flag were added at 3.1. (The older lineage shipped a Go implementation; the C rewrite is what shrank the binary and removed the Go-runtime overhead.) Note this is the pause-image version, which is independent of the Kubernetes minor version — a given Kubernetes release simply pins a recommended pause-image tag. Static-linked C became the standard. There is no policy reason to use a non-standard pause binary today.
  • Privileged-vs-not. Older pause images shipped with setuid defaults; modern builds run as non-root with no capabilities (the pause binary needs no capabilities — it does nothing).

For “shared namespaces between sibling containers” outside Kubernetes:

  • Docker --network=container:<other> is the closest analog: a Docker container joins another container’s network namespace explicitly. This is essentially what Kubernetes does with the pause container, exposed at the Docker CLI.
  • systemd-nspawn runs containers as systemd-managed services and has its own namespace-sharing model via MachineSection; not interoperable with K8s.

For “Pod-as-VM” architectures (Kata, Firecracker-MicroVM, AWS Fargate): the entire VM serves as the namespace anchor; there is no separate pause container inside the guest because the VM kernel itself plays that role. The OCI-level pause container is created on the host but is effectively a no-op — its namespaces are subsumed by the VM boundary. This is why Fargate pods don’t show a separate pause container in ps.

For “shared infrastructure across multiple pods” (the network/sidecar mesh case): that’s what a DaemonSet solves, not a different pause-container design. A node-level mesh proxy as a DaemonSet pod, plus per-pod sidecars or eBPF-based dataplanes, is the modern path; the pause container’s per-pod scope is unchanged.

Production Notes

The Borg lineage. The pause container’s design comes directly from Borg’s per-alloc placeholder process (Burns et al. 2016 ACM Queue). Borg’s “alloc” had a placeholder task that held resources even when the alloc was empty; K8s’s pause container is the same idea in container-shaped form. Borg’s lesson — that the resource-holder must exist independently of the workload — informed the choice not to merge the responsibility into kubelet itself.

Sandbox-image misconfigurations cause “first pod won’t start” incidents. The recurring operational failure is nodes unable to create new pod sandboxes after a kubelet/runtime version bump, because the containerd sandbox_image (or CRI-O pause_image) was left pointing at a pause tag that no longer resolves. The kubelet-versus-runtime split that makes this possible is the substance of kubernetes#106903. The fix is a single TOML edit per node. The lesson: keep the runtime’s sandbox_image setting aligned with the kubelet’s expected pause version during upgrades.

Uncertain

Verify: a specific dated, attributable post-mortem of a pause-image-skew outage. Reason: the symptom pattern is well-documented in upstream issues (e.g. kubernetes#106903), but I could not pin this to a named company incident report with a date. To resolve: cite a concrete public post-mortem if one is needed for this claim; otherwise treat it as the documented-but-unattributed failure pattern it is.

Air-gapped clusters always mirror pause. Every K8s-on-disconnected-network playbook includes “mirror registry.k8s.io/pause:3.X to the internal registry and set sandbox_image to point at the mirror.” Forgetting this is the canonical “first pod fails to start” failure on air-gapped installs.

Observability. crictl pods lists pod sandboxes (i.e., the existence of a pause container is equivalent to the sandbox being created). kubectl describe pod events Created Pod sandbox and Failed to create pod sandbox correspond to pause-container creation events. Cluster auditing tools that walk the process tree (Falco, Sysdig) explicitly recognize /pause and either filter it (it does nothing interesting) or use it as a Pod-membership anchor.

OpenShift’s infra containers. OpenShift’s CRI-O calls these “infra containers” rather than “pause containers” and historically used a slightly different image (openshift/pod). The role is identical to the upstream pause container.

See Also

  • Pod — the Pod abstraction whose mechanical reality the pause container is
  • Pod Networking — what gets wired into the netns the pause container owns
  • Container Network Interface — the plugin that operates on the pause container’s netns
  • Container Runtime Interface — the gRPC surface where RunPodSandbox lives
  • containerd — the most common runtime that creates and manages pause containers
  • CRI-O — alternative CRI runtime with the same pause-container semantics
  • runc — the OCI runtime that actually executes the pause binary
  • Open Container Initiative Runtime Spec — the namespace primitives the pause container holds
  • Init Containers — different concept (sequential setup before main containers); the pause container is per-Pod infrastructure, not a Pod-spec container
  • Sidecar Containers — user containers that share the pause container’s namespaces just like main containers do
  • Static Pods — pods kubelet runs directly; their pause containers behave identically to dynamic-pod pause containers
  • Kubernetes MOC — the parent MOC
  • Container Orchestration Architecture — the platform context