Skip to content
Go back

crun vs runc vs youki: OCI Runtimes Compared

By SumGuy 9 min read
crun vs runc vs youki: OCI Runtimes Compared

The Thing Docker Never Told You About

Every time you run docker run, something deeper than Docker actually spawns your container. Docker talks to containerd, containerd calls a lower-level tool, and that tool is what actually sets up namespaces, cgroups, and the pivot root that makes a container a container.

That tool is an OCI runtime. And you almost certainly have runc installed right now, running everything quietly in the background, and have never thought about it once.

Here’s the thing though — runc isn’t your only option. There’s crun (written in C, dramatically faster startup) and youki (written in Rust, newer, growing fast). Swapping them out takes about three minutes. Whether you should swap them out depends on what you’re actually building.

Let’s dig in.

What “OCI Runtime” Actually Means

The Open Container Initiative (OCI) defines a standard spec for how container runtimes must behave: given a filesystem bundle and a config JSON, create a running container. The spec covers namespaces, cgroups, capabilities, mounts — all the kernel primitives.

An OCI runtime is any program that implements that spec. You hand it a bundle directory with config.json and rootfs/, it spawns the container.

Terminal window
# What containerd actually calls under the hood
runc run --bundle /run/containerd/io.containerd.runtime.v2.task/default/abc123 abc123

Your container orchestrator (Docker, containerd, Podman, CRI-O) is the high-level runtime — it handles images, networking, storage. The OCI runtime is the low-level runtime — it’s the last mile between “we have a filesystem bundle” and “the process is running.”

This separation is why you can swap the low-level piece without touching your toolchain. Docker doesn’t care if runc or crun spawns your containers, as long as the OCI spec is satisfied.

The Three Contenders

RuntimeLanguageDefault forCgroups v2WASM support
runcGoDocker, containerdYes (since 1.0.1)No
crunCPodman, CRI-OYes (native)Yes (with wasmedge)
youkiRustNothing (yet)YesExperimental

runc: The Reference Implementation

runc is the OCI reference implementation. The Cloud Native Computing Foundation maintains it. It’s what the spec was written against, and it’s what ships as the default in Docker and containerd.

The good: It’s battle-tested. Security researchers have been poking at it for years. Enterprise support. If something goes wrong with runc, there are thousands of blog posts, issue threads, and CVE analyses covering exactly that situation.

The less good: It’s written in Go, which means startup overhead. Go’s runtime itself needs to spin up before runc can do anything. On a busy Kubernetes node spinning up 100 containers per second, that overhead accumulates. There’s also a historical dynamic linking issue — runc ships as a mostly-static binary to avoid glibc pain, which makes it heavier than it needs to be.

Terminal window
# Check your current runc version
runc --version
# See what Docker is using
docker info | grep -i runtime

Use runc when: You want zero surprises. Production Kubernetes with conservative upgrade policies. Anything where “it’s the default” is a feature, not a cop-out.


crun: The Fast One

crun is written in C by Giuseppe Scrivano at Red Hat. It started as a performance experiment — “what if we rewrote runc in C?” — and ended up becoming the default runtime for Podman and CRI-O.

Startup time difference: In benchmarks on commodity hardware, crun starts containers 2–3x faster than runc for very short-lived workloads. The binary is also dramatically smaller: runc is ~10 MB, crun is ~400 KB. On a system with 10,000 pod cold-starts per minute, that startup delta is not academic.

Terminal window
# Install crun (Fedora/RHEL)
sudo dnf install crun
# Install crun (Ubuntu/Debian)
sudo apt install crun
# Verify
crun --version

crun has native, first-class cgroups v2 support — it was designed around it, not retrofitted. If you’re on a modern Linux distro (Fedora 31+, Ubuntu 22.04+, Debian 11+) with unified cgroup hierarchy, crun handles it cleanly without the compat shims runc needed through its transition period.

It also supports WebAssembly modules as OCI bundles via the WasmEdge integration — which sounds like a novelty until you realize some people are actually shipping WASM workloads in containers at the edge.

Terminal window
# Use crun with Docker directly
docker run --runtime crun hello-world
# Or set as default in daemon.json
/etc/docker/daemon.json
{
"default-runtime": "crun",
"runtimes": {
"crun": {
"path": "/usr/bin/crun"
}
}
}
Terminal window
sudo systemctl restart docker
docker info | grep -i runtime

Use crun when: You’re running Podman (it’s already your default), you need cgroups v2 support without headaches, or you’re doing high-density workloads where startup latency actually shows up in your metrics.


youki: The Rust Contender

youki (Japanese for “container”) is a Rust implementation of the OCI spec. It’s younger than both runc and crun, but it’s past the “weekend experiment” phase — the project is active, it passes the OCI conformance tests, and it’s usable in production for adventurous shops.

Why Rust? Memory safety without garbage collection pauses. In theory, youki gets crun-level startup performance (no GC overhead) with Rust’s compile-time safety guarantees. In practice, benchmarks show youki and crun are close, with crun still edging ahead in some workload profiles.

Terminal window
# Install from GitHub releases
YOUKI_VERSION=$(curl -s https://api.github.com/repos/containers/youki/releases/latest | grep tag_name | cut -d'"' -f4)
curl -LO "https://github.com/containers/youki/releases/download/${YOUKI_VERSION}/youki_${YOUKI_VERSION}_linux.tar.gz"
tar xzf youki_*.tar.gz
sudo mv youki /usr/local/bin/
youki --version

Swap it into containerd:

/etc/containerd/config.toml
version = 2
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "youki"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.youki]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.youki.options]
BinaryName = "/usr/local/bin/youki"
Terminal window
sudo systemctl restart containerd

Or use it in Podman:

Terminal window
podman run --runtime /usr/local/bin/youki hello-world

Use youki when: You’re a Rust shop and want runtime provenance that matches your stack. You want to contribute to an active open-source project in a space that matters. Or you’re just curious and your homelab is your playground — which, honestly, is a perfectly valid reason.


The Benchmark Reality Check

Startup time dominates the conversation, but the numbers only matter in context.

For a single container that runs for hours? Runtime startup is noise. For a serverless-style system running 10,000 short-lived containers per minute? A 2ms difference per container is 20 full seconds of latency per minute, system-wide. That’s not theoretical — that’s the difference between “our autoscaler keeps up” and “we’re constantly behind.”

Rough startup benchmarks (simple echo hello workload, idle server, averages over 1000 runs):

RuntimeAvg startup (ms)
runc 1.1.x~38 ms
crun 1.x~14 ms
youki 0.3.x~16 ms

Your numbers will vary. Run hyperfine on your own hardware:

Terminal window
# Install hyperfine if you don't have it
cargo install hyperfine
# or: apt install hyperfine
# Benchmark runc vs crun
hyperfine \
'runc run --bundle /tmp/test-bundle test-runc' \
'crun run --bundle /tmp/test-bundle test-crun' \
--runs 100

Memory footprint at idle is also worth measuring if you’re running dense workloads on memory-constrained nodes.


How to Swap Runtimes Without Breaking Things

Docker

/etc/docker/daemon.json
{
"default-runtime": "crun",
"runtimes": {
"runc": {
"path": "/usr/bin/runc"
},
"crun": {
"path": "/usr/bin/crun"
},
"youki": {
"path": "/usr/local/bin/youki"
}
}
}

Keep runc registered even if it’s not default — you can always fall back with docker run --runtime runc.

containerd (Kubernetes)

/etc/containerd/config.toml
version = 2
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "crun"
[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"

Podman

crun is already the default. To use runc or youki instead:

Terminal window
podman run --runtime /usr/bin/runc myimage
podman run --runtime /usr/local/bin/youki myimage

To set a permanent default, edit ~/.config/containers/containers.conf:

[engine]
runtime = "/usr/bin/crun"

Security Considerations

Swapping runtimes doesn’t change your security surface dramatically — the kernel primitives are the same, the attack surface is the kernel itself. But:

runc has the longest CVE history (container escapes, most notably CVE-2019-5736). This is partly because it’s the most-used and therefore most-audited. Every finding gets disclosed and patched. That’s a feature.

crun is smaller — less code, smaller attack surface. C means memory safety bugs are possible in theory, but the codebase is compact and actively reviewed.

youki gets Rust’s memory safety guarantees, which eliminates a whole class of potential vulnerabilities. But it’s younger, which means less real-world fuzzing and adversarial review.

For most workloads, the runtime isn’t your container security problem. Misconfigured capabilities, writable mounts, running as root, and overprivileged service accounts are all way higher on the list. But if you’re running untrusted code or multi-tenant workloads, pair any runtime choice with proper seccomp profiles and drop capabilities explicitly.


The Honest Recommendation

If you’re running Podman: crun is already there. You don’t need to do anything.

If you’re running Docker or containerd in production: Start with runc. It’s what your team knows, it’s what your runbooks assume, it’s what your incident responders have dealt with before. When you have a concrete reason to care about startup latency — and you’ve measured it on your actual workload — then swap to crun. Not before.

If you’re experimenting or running a homelab: Try youki. The worst case is “it doesn’t work on this weird setup and you switch back,” which takes three minutes. The upside is you learn how OCI runtimes actually work, and you get to say you ran containers with a Rust runtime, which is worth something at the right kind of party.

There’s no prize for running the fastest runtime if your containers are idle 99% of the time. But there’s also no good reason to not have crun installed and ready for the workloads that actually benefit from it.

Your 2 AM self, watching a Kubernetes autoscaler fail to keep up with a traffic spike, will appreciate having thought about this once before it mattered.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Argo Workflows vs Tekton

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts