MicroVMs and Unikernels

We ended containers vs VMs with a genuine dilemma. Containers give you millisecond starts and huge density but a weak, shared-kernel boundary. VMs give you a hard, hardware-enforced boundary but seconds to boot and hundreds of megabytes each. When you must run untrusted code — a stranger's serverless function, a multi-tenant CI job — you seem forced to choose between "fast but leaky" and "safe but heavy".

This final lesson is about the engineers who refused the dilemma and built new points on the spectrum. Three ideas close the isolation gap without paying the full VM tax: gVisor (a user-space guest kernel that intercepts syscalls), microVMs like Firecracker (a hypervisor stripped to the studs, booting a real kernel in ~125 ms), and unikernels (compile the app and only the OS libraries it needs into one tiny image with no syscalls at all). Each trades a little overhead for a lot of isolation, and together they turn the binary "container or VM?" into a rich spectrum you can dial along.

The isolation ⇄ overhead spectrum

The right way to hold all of this is a single line, ordered by how strong the isolation boundary is — and, because it is the same axis, by how much overhead each option carries. From a bare process (no isolation, zero overhead) to a full VM (hardware boundary, seconds and hundreds of MB), each rung buys isolation with overhead.

ApproachIsolation boundaryBootOverheadWhere it's used
Processnone (same OS view)<1 mszeroordinary programs
Containernamespaces on a shared kernel~msnear zeromicroservices, packaging
gVisoruser-space guest kernel intercepts syscallstens of mssyscall-heavy: noticeableGKE Sandbox, App Engine
microVMreal guest kernel, minimal hypervisor~125 msa few %AWS Lambda, Fargate
Unikernelsingle-purpose image on a hypervisor~mstiny (no syscalls)appliances, edge, research
Full VMhardware-enforced, full guest OSsecondsa few %, big memorygeneral virtualization, IaaS

Notice that gVisor, microVMs, and unikernels all land between the container and the full VM — that middle band, "container-fast with VM-ish isolation", is exactly the gap they were invented to fill.

gVisor — a guest kernel in user space

Google's gVisor takes a startling approach: instead of letting the container's syscalls hit the host kernel, it intercepts them and services them in a sandbox process called \texttt{Sentry} — essentially a re-implementation of the Linux kernel's syscall interface, written in Go, running in user space. The container thinks it is talking to Linux; it is really talking to gVisor, which handles most syscalls itself and only makes a small, tightly restricted set of calls to the real host kernel on its behalf.

The payoff is a dramatically smaller host-kernel attack surface: the container can no longer reach hundreds of host syscalls directly, only the handful gVisor forwards. The cost is performance — every intercepted syscall is now a user-space round trip through the Sentry, so syscall-heavy workloads (lots of tiny I/O) slow down noticeably, while compute-bound workloads barely notice. It is the microkernel trade-off from earlier in the course, reincarnated for container isolation: put a software layer between the workload and the kernel, pay in indirection, gain in safety.

Firecracker microVMs and unikernels, pictured

Firecracker asked: why is a VM slow to boot? Not because hardware virtualization is slow — that part is fast — but because general-purpose VMMs (QEMU) emulate a whole PC: BIOS, PCI, dozens of legacy devices. Firecracker throws almost all of it away, keeping a minimal device model (a few virtio devices, a serial port) and a stripped boot path. The result is a real guest kernel with a hardware-enforced boundary that boots in about 125 ms and adds only a few MB of overhead — the isolation of a VM at nearly the agility of a container. It runs under AWS Lambda and Fargate, launching millions of microVMs to isolate every tenant's function.

A unikernel attacks from the opposite side. Rather than shrink the VM around a general OS, it shrinks the OS to nothing but what one app needs: you compile the application together with just the library-OS pieces it calls (a TCP stack, a scheduler, a filesystem — as libraries) into a single image that runs directly on the hypervisor in one address space. There is no user/kernel split and no syscalls — a "syscall" is just a function call. The image is tiny (megabytes), boots in milliseconds, and has a minuscule attack surface because code the app never uses simply isn't there. \texttt{MirageOS} (OCaml) and \texttt{Unikraft} are the flag-bearers.

Three different shapes, one shared goal: shrink what the untrusted code can touch. gVisor shrinks the reachable host kernel; Firecracker shrinks the VM around a real kernel; the unikernel shrinks the OS itself down to one app.

Comparing boot time and attack surface

Let's put rough numbers on the spectrum and let the ordering emerge. The simulation models each approach by two coordinates — a boot time and a relative "attack-surface" score (how much host-kernel code the untrusted workload can reach) — and sorts them, so you can see the trade curve rather than memorise a table.

// Rough model of the isolation<->overhead spectrum. attackSurface = relative host-kernel code // reachable by untrusted code (lower = safer); bootMs = cold start. Numbers are illustrative. interface Approach { name: string; bootMs: number; attackSurface: number; } const approaches: Approach[] = [ { name: "container", bootMs: 30, attackSurface: 100 }, // full host syscall table { name: "gVisor", bootMs: 120, attackSurface: 20 }, // only forwarded syscalls { name: "microVM", bootMs: 125, attackSurface: 8 }, // hw boundary + minimal device model { name: "unikernel", bootMs: 20, attackSurface: 5 }, // no syscalls, tiny image { name: "full VM", bootMs: 2500, attackSurface: 8 }, // hw boundary, but big general OS ]; // Order by isolation strength (smaller attack surface = stronger). const bySafety = [...approaches].sort((a, b) => a.attackSurface - b.attackSurface); console.log("Ranked by isolation (safest first):"); for (const a of bySafety) { const bar = "#".repeat(Math.max(1, Math.round(a.attackSurface / 5))); console.log(` ${a.name.padEnd(10)} surface ${String(a.attackSurface).padStart(3)} ${bar.padEnd(20)} boot ${a.bootMs} ms`); } // The sweet spot for untrusted, fast, dense workloads: strong isolation AND low boot. const sweet = approaches .filter((a) => a.attackSurface <= 10 && a.bootMs <= 200) .map((a) => a.name); console.log(`\nStrong isolation (surface ≤ 10) AND fast boot (≤ 200 ms): ${sweet.join(", ")}`); console.log("=> exactly why AWS built Firecracker for Lambda, and why unikernels intrigue researchers.");

The design law

For years the answer was awkward: Lambda packed functions into containers, but multi-tenant containers on a shared kernel are a security worry, so AWS actually ran one full VM per customer and packed that customer's functions inside — safe, but wasteful and slow to scale. Firecracker dissolved the compromise. Now each function instance gets its own microVM: a real, hardware-isolated guest kernel that boots in about 125 ms and costs only a few MB, so AWS can give every invocation VM-strength isolation and still start it fast enough to feel serverless. Firecracker is open-source, written in Rust for memory safety, and deliberately tiny — around 50{,}000 lines versus QEMU's millions — because in a security boundary, less code is less to get wrong. It is the whole "isolation vs overhead" course lesson turned into a shipping product.

Two traps. First, do not read this lesson as "microVMs beat containers, always use them." Every rung up the isolation ladder costs something — gVisor taxes syscall-heavy workloads, microVMs add ~125 ms and a few MB per instance, unikernels demand you rebuild your app against a library OS and lose the rich debugging and tooling of a normal Linux. If your tenants already trust each other, a plain container is the right, cheapest tool. Second, a unikernel is not a lightweight container: it has no shared host kernel, no \texttt{ps}, no shell, no multiple processes — it runs one program in one address space on a hypervisor. That is its strength (tiny attack surface) and its pain (no familiar OS underneath, a hard porting story). Match the isolation mechanism to the threat model and the workload — the point of a spectrum is that you get to choose where to stand, not that one end wins.