Skip to content

Engine, Chassis, Runtime

The most common question we get about the sandbox runtime is some variation of "why Firecracker and not Kata?" or "should we switch to Cloud Hypervisor?" Both questions contain the same hidden assumption: that Firecracker, Cloud Hypervisor, and Kata are three interchangeable options at the same layer of the stack, and picking one means not picking the others.

They are not at the same layer. Firecracker and Cloud Hypervisor are engines. Kata is a chassis you drop an engine into. Asking "Firecracker or Kata" is like asking "a V8 or a sedan" — the sedan has a V8. This page lays out the layer cake, shows exactly where AerolVM's four runtimes sit in it, and explains why AerolVM ended up writing its own chassis instead of adopting one off the shelf.

Four layers, bottom to top:

AerolVM service layer (internal/service) ← orchestration
┌───────────┼──────────────┐
runc gVisor Kata ← OCI RUNTIMES
(docker) (runsc) (kata-runtime) "how is a container isolated?"
│ │ │
namespaces userspace launches a real VM
kernel │
┌─────────┼──────────┐
Firecracker Cloud Hyp. QEMU ← VMMs / "hypervisors"
└─────────┼──────────┘ the actual engine
KVM ← kernel CPU virtualization
  • KVM is the Linux kernel's CPU-virtualization primitive. Everything with a real VM sits on it.
  • A VMM (Virtual Machine Monitor) is the userspace process that creates the VM, emulates its devices, and boots its kernel. Firecracker and Cloud Hypervisor live here, and they are peers. Both are minimal Rust VMMs built on the shared rust-vmm crates; both are driven by a REST API over a Unix socket. Cloud Hypervisor even began from Firecracker's rust-vmm components.
  • An OCI runtime answers a different question: given an OCI container, how do I isolate it? runc uses Linux namespaces. gVisor (runsc) uses a userspace kernel that intercepts guest syscalls. Kata boots the container inside a real VM — using a VMM underneath. Kata's hypervisor is a config knob: Firecracker, Cloud Hypervisor, or QEMU.
  • Orchestration is whatever drives the OCI runtime — Docker, containerd, Kubernetes, or in AerolVM's case the service layer.

The punchline sits between the middle two layers: Kata does not compete with Firecracker. Kata runs on top of Firecracker. The moment you internalize that, "Firecracker vs Kata" dissolves — they're answering questions one layer apart.

Where AerolVM's four runtimes actually sit

Section titled “Where AerolVM's four runtimes actually sit”

AerolVM ships four runtimes today. They do not line up in a single row — they're spread across two different layers:

RuntimeLayerIsolation mechanism
Docker (runtime: "docker")OCI runtime (runc)Linux namespaces + cgroups
gVisor (runtime: "gvisor")OCI runtime (runsc)Userspace kernel intercepting syscalls
Firecracker (runtime: "firecracker")Our own OCI-runtime-equivalent → VMMHardware VM (KVM)
WASM (runtime: "wasm")A different axis entirelyLanguage VM / sandboxed bytecode

Docker and gVisor are drop-in OCI runtimes — the container ecosystem handed them to us. WASM is off the diagram: it isolates at the language-VM layer, not the container layer, so it trades syscall compatibility for the smallest possible footprint (see WASM Architecture).

Firecracker is the interesting one. There is no off-the-shelf OCI runtime in that row, because we wrote it ourselves.

We did not set out to build Kata. But internal/runtime/firecracker is an OCI-runtime-equivalent — a bespoke, minimal Kata specialized for one engine. Everything Kata would have given us off the shelf, we hand-rolled directly against the Firecracker VMM:

What we builtWhereThe job it does
VMM REST clientpkg/firecracker/client.goOne client per VMM socket; one Go method per Firecracker resource (machine-config, boot-source, drives, net, vsock, actions, snapshot)
Runtime driverinternal/runtime/firecracker/driver.goThe runtime.Runtime surface — Create/Start/Stop/Destroy. This is literally our Kata.
Process supervisorinternal/runtime/firecracker/vmm.goSpawn the firecracker binary, wait for its API socket, SIGTERM→SIGKILL teardown, capture a stderr tail for post-mortems
Jailer wrapinternal/runtime/firecracker/jailer.gochroot + cgroups + drop-privilege isolation around the VMM process
Image → bootable rootfspkg/oci/builder.goTurn docker://python:3.11 into a rootfs.ext4 via skopeo → umoci → mkfs.ext4
Host networkinginternal/network/tap/TAP device + IPv4 slot + vsock CID allocator, one per VM
Snapshot capture + warm poolinternal/runtime/firecracker/snapshot.go, warmspawn.go, internal/pool/vmm/Pre-boot VMMs from a template snapshot, leave them paused, and hand one to a create call for a sub-100ms boot

Every row except the last is something Kata provides. So why not just use Kata and delete most of that table?

Because of the last row.

AerolVM's fast-boot story is snapshot-load: a warm pool of VMMs that were already booted from a template snapshot and left paused, so a create call claims one, PATCHes per-sandbox state onto it, and resumes it — skipping the kernel boot entirely. That path lives in warmspawn.go and the warm pool, and it is only possible because we speak the Firecracker snapshot API directly.

Kata abstracts the VMM away on purpose — that is its whole value. But that abstraction is exactly what would hide the paused-resume snapshot mechanics our warm pool is built on. Adopting Kata would mean trading our differentiator for the containerd/Kubernetes ecosystem. That is a strategic pivot toward the Kubernetes world, not a capability upgrade — and it is a very different decision from swapping the engine underneath. The correctness invariants that make this safe (every clone must be unique before its vCPUs run guest code) are their own deep dive: see The Frozen-Kernel Problem and Snapshot Clone Correctness.

Firecracker vs Cloud Hypervisor: swapping the engine

Section titled “Firecracker vs Cloud Hypervisor: swapping the engine”

Now the sibling comparison — the one that genuinely is same-layer. Both are minimal Rust VMMs on rust-vmm + KVM, both driven by REST over a Unix socket. They differ in philosophy: Firecracker optimizes for minimal attack surface and density; Cloud Hypervisor keeps the fast-boot core but adds back the devices real workloads need.

FirecrackerCloud Hypervisor
OriginAWS (powers Lambda, Fargate)Intel-founded, now community
Design goalSmallest surface, max densityMore capable, modern workloads
Device modelvirtio over MMIO, minimal setFull PCI/PCIe
VFIO passthroughNoYes — GPU / NIC passthrough
Hotplug (CPU/mem/PCI)Essentially noneYes
Confidential computeNoAMD SEV-SNP, Intel TDX
Firmware / bootDirect vmlinux bootUEFI + ACPI (boots more OSes, incl. Windows)
Live migrationNoYes
Boot time~125 msSub-second (slightly heavier)
Memory overhead~5 MiB/microVMSlightly higher, still small
SnapshotsFirst-class (our warm pool)Supported

The strategic reading: reach for Cloud Hypervisor when you need something Firecracker structurally cannot do — GPU passthrough for inference sandboxes, confidential VMs (SEV-SNP / TDX) for the "the host operator cannot read guest memory" tier, or bigger, longer-lived VMs with hotplug.

And here is the payoff of having built our own chassis: adding Cloud Hypervisor is swapping the engine, not the car. A second pkg/cloudhypervisor/client.go mirrors pkg/firecracker/client.go (same HTTP-over-Unix-socket shape), and a second driver reuses everything else we already own — pkg/oci, internal/network/tap, the process-supervisor pattern, even the warm-pool seam. Because Cloud Hypervisor also supports snapshots, the fast-boot trick carries over intact.

Critically, none of this touches the SDK. Runtime selection is one string in the create call — the same field that already distinguishes Docker, gVisor, Firecracker, and WASM. A new engine slots in behind it without changing a line of client code:

import { MicroVM } from '@aerol-ai/aerolvm-sdk'
const client = new MicroVM({
apiUrl: process.env.SB_API_URL,
patToken: process.env.SB_PAT_TOKEN,
})
// Runtime is a string. Today it selects docker | gvisor | firecracker | wasm.
// A new VMM (e.g. cloud-hypervisor) would slot in behind this same field
// without any SDK change.
const sbx = await client.create({
name: 'gpu-inference',
image: 'pytorch/pytorch:latest',
runtime: 'firecracker',
cpu: 4,
memoryMB: 8192,
})
console.log(`created ${sbx.id} on the firecracker VMM`)

The whole point of the layer cake is that isolation is two decisions, not one. First pick how strongly you want to isolate — which layer. Then, if that layer is a VM, pick which engine.

You wantPick this layerNotes
Max compatibility, lowest overhead, trusted-ish coderunc (Docker)Namespaces. Fastest, weakest boundary.
Untrusted code, container UX, no VM costgVisor (runsc)Userspace kernel. Strong boundary, some syscall-compat gaps.
Hardware-VM isolation with sub-100ms bootFirecrackerOur hand-rolled driver + snapshot warm pool. Default microVM.
GPU passthrough / confidential VMs / hotplugCloud Hypervisor (candidate engine)Same layer as Firecracker; swap the engine, reuse the chassis.
Smallest possible footprint, per-request isolationWASMLanguage-VM layer. Different tradeoff axis entirely.

The mental model to walk away with: Firecracker and Cloud Hypervisor are engines; Docker/gVisor/Kata are chassis; AerolVM built its own chassis so it could drive the engine directly and keep the snapshot warm pool. That single architectural choice is what makes adding a second VMM a contained change instead of a rewrite — and why "should we use Kata?" and "should we add Cloud Hypervisor?" are not the same question at all.