all posts

Firecracker vs unikernels: two ways to shrink a VM

Ajay Kumar··9 min read

"Firecracker vs unikernels" sounds like two answers to the same question — how do I make a VM small and fast? — and it is, except they shrink opposite ends of the stack and end up in completely different places. A unikernel makes the guest tiny by throwing away the operating system and compiling just the slivers your one app needs into a single bootable image. Firecracker keeps a full, boring Linux guest and instead makes the emulator around it tiny. Both boot fast. Both have a small footprint. But that one architectural fork decides something that matters enormously if you run other people's code: whether the thing can run a binary you haven't seen yet.

I'm Ajay — I build PandaStack, an open-source Firecracker microVM platform — so I've spent a lot of time on exactly this trade-off, because the whole product is about running arbitrary, untrusted, often model-generated code safely. This post is a fair head-to-head. Unikernels are genuinely great at what they're for, and I'll say so clearly. But they and Firecracker are answering different questions, and picking the wrong one for your workload is an expensive mistake in either direction.

What a unikernel actually is

A unikernel is your application linked together with a minimal library operating system into one image that boots directly on a hypervisor. There's no separate kernel, no init, no user accounts, and — usually — no notion of a second process. The 'libOS' supplies only what your app calls: a TCP/IP stack, a scheduler, a memory allocator, maybe a filesystem, exposed as ordinary function calls rather than as a syscall trap into a privileged kernel. Everything runs in a single address space at a single privilege level, which means there's no user/kernel boundary to cross and no syscall overhead — a network call is just a function call into a library that happens to be a network stack.

The landscape is varied, and the projects make different bets. Unikraft is a modular POSIX-ish libOS that aims for broad app compatibility by composing the pieces you need. MirageOS goes furthest into purity: the whole thing is written in OCaml as type-safe modules you compile in or leave out, so the image contains provably nothing you didn't ask for. OSv targets managed runtimes — run a JVM or a Node app on a thin single-address-space OS. Nanos (the kernel behind NanoVMs) leans the other way, running unmodified Linux ELF binaries in a single-process unikernel. They share the core idea — one app, one image, no general-purpose OS — and differ enormously in how much of a real userland they try to emulate. (All of these move fast; check each project's own docs for its current capabilities.)

The one-line summary: a unikernel is small because it deleted the operating system. A Firecracker microVM is small because it shrank the emulator and kept a normal operating system inside. The first optimizes the guest; the second optimizes the boundary.

The real appeal: tiny, fast, almost no surface

Take unikernels seriously, because the wins are real and not marketing. An image can be a few megabytes because it contains one program and the exact library code it touches — no shell, no package manager, no cron, no SSH daemon you forgot was installed. Boot times are excellent; published benchmarks put well-tuned unikernels in the sub-millisecond range for the guest itself, because there's almost nothing to initialize (verify the current figures against each project's docs — they vary widely by libOS and hardware). And the attack surface is genuinely minimal: if there's no `exec`, no second process, and no shell, an entire category of exploitation — pop a shell, fork a payload, escalate through the user model — simply isn't available, because the machinery those attacks rely on was never compiled in.

For a single, purpose-built, trusted service, that combination is beautiful. An edge network function, a latency-sensitive microservice you own end to end, a security appliance where 'there is deliberately no shell' is a design goal — for those, the minimalism is the entire point, and a unikernel delivers something a full Linux guest can't match in sheer economy. This is not a straw man. If your program is fixed and yours, a unikernel is a legitimately excellent answer.

The practical cost: the OS you deleted was doing a lot

The trouble starts the moment your workload wants something the OS used to provide for free. Single address space means no hard memory isolation between your app and the libOS — a memory-safety bug that a real kernel would contain to one crashing process can corrupt the whole image, because there is no 'other process' to protect. The POSIX model you lean on without thinking — `fork`, `exec`, a second process, users and permissions, a real shell — is partial to absent depending on the project. Some emulate a heroic subset; none give you the complete thing, because giving you the complete thing would mean re-implementing the OS you were trying to avoid.

Then there's day two. A unikernel has no shell, which is either a security feature or the worst on-call night of your life, depending — when something misbehaves you can't SSH in and poke around, because there's nothing to SSH into. You're back to serial-console printf and specialized tracing. The ecosystem is thin: your APM agent, your sidecar, your favorite ops tool all assume a Linux userland that isn't there. Driver support is narrower. And you must rebuild to change anything — the app is baked in, so a config tweak or a dependency bump is a recompile-and-redeploy, not a `pip install`. None of this is fatal for the right workload. It's just the bill for deleting the OS, and you pay it whether or not you expected to.

The deal-breaker for a whole class of platforms: a unikernel is compiled around a known program. You cannot compile a unikernel out of code you haven't seen yet — and 'code you haven't seen yet' is the entire job description of an AI-agent sandbox, a code interpreter, or a user-upload runner.

The Firecracker bet: keep the OS, shrink the emulator

Firecracker makes the opposite trade. Instead of stripping down the guest, it strips down the Virtual Machine Monitor — roughly 50k lines of memory-safe Rust on KVM, with a deliberately tiny device model (virtio-net, virtio-block, virtio-vsock, a serial console) and no BIOS, no PCI bus, no USB. Inside that minimal box it runs a completely normal, if stripped, Linux guest: a real guest kernel, real processes, a real shell, the full Linux syscall ABI. You get hardware-virtualization isolation — the security boundary is the small, audited VMM, not a shared host kernel — and you get to run literally anything you can put in a rootfs, because inside the box it's just Linux.

That's the property that makes running arbitrary code possible. An AI agent can fork a shell, `pip install` a package, spawn subprocesses, write files, and run a binary nobody at your company has ever read — all against a real Linux kernel that behaves the way every Linux program expects, inside a guest that is one hardware-virtualization escape (not a mere process boundary) away from anything else on the host. Firecracker doesn't try to make the guest minimal or known-in-advance. It makes the guest disposable and the wall around it strong, and it keeps the full Linux ABI so that 'whatever the code turns out to be' just works.

Side by side

  • Isolation model — Unikernels: hypervisor-isolated from the host, but a single address space internally, so there's no app-vs-libOS memory protection inside the image. Firecracker: hardware virtualization is the boundary and the guest is a normal, internally-protected multi-process Linux VM.
  • Linux ABI compatibility — Unikernels: partial to absent; the libOS reimplements a chosen subset (Nanos runs Linux ELFs, Unikraft targets POSIX compat, MirageOS is its own OCaml world). Firecracker: full, real Linux ABI — anything that runs on Linux runs in the guest, unmodified.
  • Boot & footprint — Unikernels: often tiny images and sub-millisecond guest boots in published benchmarks (verify), little to initialize. Firecracker: a full-OS image and a few MB of per-guest kernel/device overhead, but snapshot-restore drives per-create latency down to the sub-second range in practice.
  • Debuggability — Unikernels: serial-console-first, no shell to SSH into, specialized tooling. Firecracker: standard Linux — SSH, ps, strace, APM agents, everything you already know works inside the guest.
  • Attack surface — Unikernels: minimal; often no shell, no exec, no second process to attack. Firecracker: a small VMM boundary, but a full Linux userland inside the guest to harden.
  • Best fit — Unikernels: a single trusted binary you own, shipped as a minimal appliance. Firecracker: dense, multi-tenant, untrusted, or model-generated Linux workloads.
  • Running untrusted / arbitrary code — Unikernels: no — you can't compile an image around a program you haven't seen. Firecracker: yes — the full Linux ABI runs whatever shows up at runtime, which is the whole reason it exists.

One nuance worth stating so the table doesn't over-simplify: at the hypervisor level this isn't strictly either/or. Some unikernels (Nanos, OSv, Unikraft) can themselves boot on Firecracker or QEMU — the unikernel is the guest, Firecracker is the VMM. So the real, decision-shaping question isn't which monitor. It's which guest model: a purpose-built single-image appliance, or a general-purpose Linux box you can drop anything into.

The insight: you can't compile a unikernel out of code you haven't seen

This is the crux, and it's not a performance point — it's a structural one. A unikernel's whole model is 'know the program at build time, link it against exactly the OS pieces it needs, ship one image.' That's precisely what you cannot do when the program is decided at runtime. An AI agent that shells out to whatever command it reasons its way to, a user uploading a Python script, a CI runner cloning a repo you've never seen, a code interpreter executing model-written cells — none of these have a binary you can compile ahead of time. They need the full, general-purpose Linux ABI to be present already, ready to run whatever turns up, because the whole point is that you don't know what will turn up.

That's why the untrusted-code use case lands on a real Linux guest behind a hardware wall rather than on a unikernel. You need two things at once: a full OS the arbitrary code can actually run against, and a boundary strong enough that a stranger's code — possibly hostile, possibly just wrong — stays contained to one disposable machine. A unikernel gives you the strong-ish boundary but not the full OS. A container gives you the full OS but shares the host kernel. A microVM gives you both.

# UNIKERNEL: you must know the program at build time and compile it in.
#   $ ops build ./my-fixed-binary            # bake THIS binary into an image
#   $ ops run   ./my-fixed-binary.img         # boots your one known app
#   ...there is no step where a user hands you code you didn't compile.
#
# FIRECRACKER (via PandaStack): the full Linux ABI is already there, so you
# run whatever shows up at runtime — a binary you never saw ahead of time.

from pandastack import Sandbox

# A general-purpose Linux guest in one Firecracker microVM, created via
# snapshot-restore (~49ms restore; ~179ms p50 / ~203ms p99 end-to-end;
# only the very first cold boot of a template is ~3s).
sbx = Sandbox.create(template="base", ttl_seconds=300)

# Arbitrary code decided at RUNTIME — the exact thing a unikernel can't do,
# because there was nothing to compile ahead of time. A real shell, a real
# package install, a real subprocess, all against a real Linux kernel.
print(sbx.exec("python3 -c 'print(sum(range(100)))'").stdout)
print(sbx.exec("pip install --quiet requests && python3 -c "
               "'import requests; print(requests.__version__)'").stdout)

sbx.destroy()   # the whole microVM goes away — nothing leaks to the next task

But doesn't the unikernel win on boot speed?

On raw cold boot, often yes — there's simply less to initialize when you've deleted the OS, and that's a fair advantage to concede. But raw cold boot isn't the number that decides a platform, because you rarely cold-boot on the hot path. Firecracker snapshots an already-booted guest and restores that snapshot on each create instead of booting from scratch. On PandaStack the restore step is around 49ms, and an end-to-end create is about 179ms p50 (roughly 203ms p99); only the very first spawn of a template pays the ~3s cold boot. That closes most of the practical gap: you're comparing a unikernel's fast cold boot against a full-Linux microVM that skips cold boot almost entirely by restoring a memory image.

The copy-on-write story goes further in the same direction. Forking a running microVM lands in roughly 400–750ms on the same host (1.2–3.5s cross-host), sharing memory and disk copy-on-write, so you can spin up many near-identical guests cheaply. And each agent pre-allocates 16,384 /30 subnets, so per-sandbox networking is set up in single-digit milliseconds rather than becoming the bottleneck. The point isn't that Firecracker beats a unikernel's cold boot — it's that snapshot-restore makes the boot-speed question mostly stop mattering for the workloads where you'd actually want the full OS.

When unikernels are the right call

Reach for a unikernel when you have a single, trusted binary and minimalism is the objective. An edge appliance, a network function, a latency-sensitive microservice you own end to end, or a posture where 'there is deliberately no shell and no way to spawn a process' is a feature you can architect around. If your program is fixed, your dependencies are known, and you value a tiny image and a tiny attack surface over the flexibility of a full OS, a unikernel gives you an economy a general-purpose Linux guest can't touch. That's a real, legitimate win, and there's genuinely no shame in shipping a two-megabyte image that boots before you finished reading this sentence.

When Firecracker wins

Firecracker wins the instant your workload isn't a fixed, trusted single binary — the instant it's 'run a lot of small, untrusted, arbitrary Linux workloads, safely, fast, and densely.' Code interpreters, per-user playgrounds, AI-agent sandboxes executing model-written commands, ephemeral CI runners for repos you've never seen, per-tenant databases. All of those need the full Linux ABI the unikernel deleted — real processes, exec, a shell, POSIX — behind a boundary strong enough to run a stranger's code. You want the general-purpose guest and the minimal wall, and that combination is exactly what a microVM is.

That's the line PandaStack is built on. Every sandbox, database, and app is its own Firecracker microVM, created via snapshot-restore at a p50 of about 179ms (roughly 203ms p99; the restore step itself around 49ms), with only the very first cold boot of a template taking around 3s. Forks land in 400–750ms on the same host (1.2–3.5s cross-host), and each agent pre-allocates 16,384 isolated /30 subnets so networking never becomes the bottleneck. We chose a full Linux guest inside a minimal microVM precisely because the workload is dense, untrusted, model-generated code — the case where you must keep the whole OS but only need a small, hard wall around it. If your workload is instead one trusted binary you'd happily ship as an appliance, a unikernel is a fine answer — and verify the current capabilities of whichever project you're weighing against its own docs before you decide.

Frequently asked questions

What's the difference between a unikernel and a Firecracker microVM?

A unikernel compiles your application together with a minimal library OS into one bootable image that runs in a single address space, with no separate kernel, no processes in the usual sense, and often no shell. A Firecracker microVM runs a full, real (if stripped) Linux guest kernel inside a tiny hardware-isolated VMM. Unikernels are small by deleting the operating system; Firecracker is small by shrinking the emulator around a normal operating system. The practical consequence: a unikernel runs one program you compiled ahead of time, while a microVM runs any Linux binary, including code decided at runtime.

Can a unikernel run arbitrary or untrusted code?

Generally no, and it's structural rather than a limitation you can tune away. A unikernel image is compiled around one known program, so there's no way to run a binary you haven't seen — and 'code you haven't seen yet' is exactly what an AI-agent sandbox, a code interpreter, or a user-upload runner has to execute. Unikernels also typically lack a full POSIX environment: no arbitrary exec, often no second process, no shell. For arbitrary or untrusted code you need the full Linux ABI present already, which is what a full-OS microVM like Firecracker gives you. Verify each unikernel project's current capabilities against its own docs.

Are unikernels more secure than a Linux microVM?

They have a smaller attack surface — no shell, no exec, no user model, minimal compiled-in code — which removes whole categories of exploitation, and that's a real advantage for a single trusted binary. But they run in one address space, so there's no memory protection between the app and the library OS; a memory-safety bug a real kernel would contain to one process can corrupt the entire image. Firecracker keeps a full multi-process OS but wraps it in hardware virtualization, so the trade-offs differ. Neither is universally 'more secure' — for a fixed trusted appliance the unikernel's minimalism shines, and for running untrusted arbitrary code the microVM's hardware boundary plus full OS is the fit.

Isn't a unikernel much faster to boot than Firecracker?

On raw cold boot, often yes — there's less to initialize when the OS is gone, and published unikernel benchmarks show sub-millisecond guest boots (verify against each project). But cold boot rarely sits on the hot path for a full-OS platform, because you snapshot an already-booted guest and restore it instead of booting from scratch. PandaStack restores at around 49ms with an end-to-end create of about 179ms p50 (203ms p99), and only the very first spawn of a template pays the ~3s cold boot. Snapshot-restore closes most of the practical boot-speed gap for the workloads where you'd actually want a full Linux OS.

When should I choose a unikernel instead of Firecracker?

Choose a unikernel (Unikraft, MirageOS, OSv, Nanos/NanoVMs) when you have a single, trusted binary you want to ship as a minimal appliance and you value a tiny image and tiny attack surface over the flexibility of a full OS — edge functions, network appliances, latency-sensitive services you own end to end. Choose Firecracker (or a platform built on it, like PandaStack) when you need to run arbitrary, multi-tenant, or model-generated code that expects a full Linux ABI, behind hardware isolation, where a bug stays contained to one disposable VM.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.