Container Runtimes in 2026: crun, containerd 2.x, and Beyond
crun has become the default low-level runtime in much of the ecosystem while containerd 2.x strips out a decade of legacy. A tour of the runtime stack, what actually changed, and what to watch next.
Published on • November 15, 2026
AI Assistant

Ask most engineers what runs their containers and you will hear “Docker” or “containerd” — and both answers skip the component that actually talks to the kernel. The low-level OCI runtime (runc, crun, youki) is a small, short-lived program that reads a JSON spec, configures namespaces, cgroups and mounts, execs PID 1, and gets out of the way. Five years ago that answer was always runc. In 2026 it is often crun, and the daemon above it is running containerd 2.x, which is a materially different beast from the 1.x line people still have muscle memory for. Here is where the runtime stack actually stands.
The stack, from kubelet to kernel
On a Kubernetes node the chain looks like this:
- kubelet talks CRI (Container Runtime Interface, gRPC) to…
- containerd or CRI-O — the node-level daemon that handles image pulls, storage, logging and pod sandboxes — which launches…
- a shim (
containerd-shim-runc-v2), a tiny long-lived process per container that survives the daemon restart and proxies I/O and lifecycle events — which spawns… - the low-level runtime (
runc,crun,youki), which does the syscall work and exits. - the Linux kernel: namespaces, cgroups v2, seccomp, Landlock, LSMs.
That layering is why you can swap step 4 without touching steps 1–3: everything implements the OCI runtime specification. It is also why the interesting debates are really about where innovation lands — in the spec, in the daemon, or in the runtime.
crun: what a runtime written in C buys you
crun was born when Giuseppe Scrivano noticed that Go is a bad fit for the fork/exec dance an OCI runtime performs: Go’s runtime expects to fork-and-exec immediately, while a runtime must fork, do a pile of namespace/cgroup/hook configuration, and only then exec — a model Go handles only through increasingly clever hacks. C is not multi-threaded by default and was built around fork/exec, so crun is both simpler and smaller.
The numbers from Red Hat’s original introduction have held up:
| Metric | runc | crun |
|---|---|---|
Binary size (-Os) | ~15 MB | ~300 KB |
100 sequential true containers | 6.89 s | 3.86 s |
| Max resident set size | 15,120 KB | 3,752 KB |
--pids-limit 1 | fails (Go runtime threads) | works |
Consequences that matter in production: crun can run containers under a 250 KB memory limit, it works where Go toolchains are weak (crun was used to port Docker to RISC-V), and because it is a C library as much as a binary — libcrun — Podman’s monitor conmon and other tooling can link against it instead of spawning a process.
crun also became the ecosystem’s innovation staging ground. Its annotation-based experiments (run.oci.keep_original_groups=1 for leaking supplementary groups into rootless containers, run.oci.hooks.stdout=/tmp/hook.stdout for debugging hooks, run.oci.systemd.force_cgroup_v1 for running legacy systemd images on cgroup v2 hosts) proved features that were then proposed into the runtime spec for runc to adopt. That loop — implement in crun, standardize, converge — is exactly how the cgroups v2 migration happened: Fedora 31 shipped crun as default and forced the whole stack to catch up while runc still had only experimental v2 support.
crun is production-default territory today: Fedora has shipped it since Fedora 31, CRI-O’s CI runs Kubernetes with it, Arch and Gentoo use it, and Docker and containerd both accept it because it is just another OCI runtime binary.
containerd 2.x: the great cleanup
containerd 2.0 was a deliberate breaking release — the project’s framing was “remove what has been deprecated across nine years” — and the 2.x line has since moved on to 2.4. The headline changes:
What was removed
- The legacy shims
io.containerd.runtime.v1.linuxandio.containerd.runc.v1. Runtime v2 (io.containerd.runc.v2) is the only path. - The CRI
v1alpha2API (deprecated in 1.7). CRI v1 only. - The AUFS snapshotter; overlayfs is the answer.
- Docker Schema 1 image pulls, disabled by default.
io_uring_enter/register/setupdropped from the default seccomp profile — a security-driven change after a stream of kernel exploits, and one that surprises apps which had been quietly using io_uring inside containers.
What changed structurally
- The Go library was split into separately versioned modules, with the gRPC API extracted to its own module (
github.com/containerd/containerd/v2/api) so the wire contract can stay stable while the daemon moves. The CRI plugin itself has shipped in-tree since containerd 1.1 and remains the Kubernetes-facing surface, but the monolith around it is now modular. - Daemon configuration moved to
version = 3, auto-migrated on startup; runcontainerd config migrateto stop paying the migration cost at boot. Deprecated CRI registry properties (mirrors,auths,configs) are on their way out in favor ofconfig_pathhosts files andimagePullSecrets. - The sandbox service went stable and Sandboxed CRI is now the default path for podsandbox handling; sandbox attributes are mutable via a new
UpdateAPI.
What shipped new
- NRI (Node Resource Interface) and CDI (Container Device Interface) are enabled by default — the two mechanisms vendors now use to inject device and resource logic into the runtime.
- Image verifier plugins on the transfer service: you can now reject pulls of unsigned or misnamed images at the daemon, not with an admission webhook.
- CRI user namespaces support (requires runc 1.2+), recursive read-only mounts, and unprivileged ports/ICMP on by default.
- OpenTelemetry environment-variable configuration for the built-in tracing plugin, plus
ctr deprecations list/ctr plugins inspect-runtimeso you can find migration blockers before they find you.
Putting crun under containerd is a three-line config change:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun.options]
BinaryName = "/usr/bin/crun"
Then reference crun from a RuntimeClass, or make it the default runtime for the node.
The CRI landscape in Kubernetes
Since dockershim was removed in Kubernetes 1.24, every node runs a CRI daemon: containerd (the mainstream default, now on the 2.x line) or CRI-O (purpose-built for Kubernetes, versions tracked one-to-one with Kubernetes releases, common in OpenShift and OpenShift-adjacent fleets). Both speak CRI v1, both delegate to a shim, and both let you pick the low-level runtime per RuntimeClass:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: crun
handler: crun
That handler field maps onto the runtime table in containerd’s config, which is how you run runc for the general fleet and, say, gVisor’s runsc or Kata Containers for untrusted multi-tenant workloads on the same nodes. The important trend: the runtime is no longer a node-wide decision but a workload-level one, selected by the scheduler via the RuntimeClass admission path.
What to watch
- youki (containers/youki), the Rust implementation of the runtime spec, is the credible third option — memory-safe, cross-platform, and a useful signal for how much of the spec can be reimplemented cleanly outside C and Go.
- NRI maturing matters more than another runc-vs-crun benchmark: it is the plugin surface that used to be solved by patching runtimes, and it now covers device injection, resource tuning and resource managers.
- Sandboxed runtimes as the multi-tenant answer. Kata (microVM-backed) and gVisor (kernel-interposition) are converging with the container stack rather than replacing it; confidential-computing variants of Kata are the ones to track for regulated workloads.
- Spec-level hardening: user namespaces by default, Landlock as a complement to seccomp, and continued pressure to keep the runtime’s job small — the shim/service split in containerd 2.x exists precisely so the daemon can restart without touching containers.
Wrapping up
The runtime layer got boring in the best possible way: crun and runc are interchangeable OCI implementations, containerd 2.x is mostly a story about deleting the past, and the real action has moved to NRI, CDI, image verification and workload-selected RuntimeClasses. If you are still on containerd 1.6/1.7, start with ctr deprecations list against a test node — it will tell you exactly what the 2.x upgrade will complain about before you schedule a maintenance window.