all posts

Firecracker vs NanoVMs: microVM vs unikernel

Ajay Kumar··9 min read

Every few years the unikernel idea comes back around, and it's genuinely seductive: why boot a whole general-purpose operating system — with its process scheduler, user accounts, shells, package manager, and a syscall interface built to run anything — when your production box runs exactly one program? Unikernels answer that by throwing the OS away and keeping only the slivers your app actually calls, compiling app-plus-library-OS into a single bootable image. "Firecracker vs NanoVMs" pits that minimalist model (NanoVMs/Nanos, OSv, MirageOS) against Firecracker's approach, which is the opposite bet: run a full, boring Linux guest inside a tiny hardware-isolated microVM. Both are small and fast to boot. They disagree completely about what to be small about.

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 — no separate kernel, no init, no processes in the usual sense. The 'libOS' provides only what the app needs: a TCP/IP stack, a scheduler, a memory allocator, maybe a filesystem, exposed as ordinary library calls rather than a syscall trap into a privileged kernel. Everything runs in a single address space at a single privilege level. NanoVMs' Nanos runs unmodified Linux ELF binaries this way; OSv targets managed runtimes like the JVM; MirageOS goes further and builds the whole thing in OCaml as type-safe modules you compile in or leave out.

The payoff of that design is real and worth taking seriously: images can be tiny, boot times are excellent (there's almost nothing to initialize), and the attack surface is genuinely minimal — there's no shell to pop, no second binary to exec, no user-account model to escalate through, often no way to spawn a new process at all. If an attacker can't fork a shell because the concept of 'another process' doesn't exist in your image, a whole category of exploitation simply isn't available. For a single, purpose-built, trusted service, that's a beautiful property.

The one-line summary: a unikernel is small by deleting the operating system. Firecracker is small by shrinking the emulator around a normal operating system. The first optimizes the guest; the second optimizes the boundary.

The appeal: one app, one image, almost no surface

Take a unikernel seriously when you have a single binary you fully control and want to ship it as an appliance. You get a bootable image measured in megabytes, sub-second (sometimes single-digit-millisecond) boots, no idle OS eating RAM in the background, and a threat model that's easy to reason about because there's so little there. No SSH daemon running by default because you didn't compile one in. No cron, no package manager, no drive-by dependency you forgot was installed. For an edge appliance, a network function, or a latency-sensitive microservice you own end to end, the minimalism is the whole point — and it delivers.

The practical pain: 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 the app and the libOS — a memory-safety bug that a real kernel would contain to one process can corrupt the entire 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 partially or entirely absent depending on the unikernel. Some projects emulate a subset heroically; none give you the full thing, because giving you the full thing would mean re-implementing the OS you were trying to avoid.

Then there's the day-two reality. Debugging is harder when you can't just SSH in and poke around, because there's nothing to SSH into — you're back to serial-console printf and hoping. The ecosystem is thin: your favorite APM agent, sidecar, or ops tool assumes a Linux userland that isn't there. Driver and hardware support is narrower. And the deal-breaker for a whole class of platforms: you cannot run arbitrary, untrusted code. A unikernel is compiled around a known program. 'Let a user upload a Python script and run it' or 'let an AI agent shell out to whatever command it decides to run' is exactly the general-purpose, multi-process, POSIX-shaped workload unikernels give up to be small. (As always, capabilities differ per project and move fast — verify the current state against each unikernel's own docs before deciding.)

Unikernels can be excellent for a single binary you own and trust. They are the wrong tool for running arbitrary or untrusted code, because that code expects a full OS — processes, exec, a shell, POSIX — which is precisely what a unikernel deletes to get small.

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, PCI bus, or USB. Inside that minimal box it runs a completely normal Linux guest with its own kernel, processes, users, shell, and full syscall interface. You get VM-grade hardware isolation (the security boundary is the small, audited VMM, not a shared kernel) and you get to run literally anything you can put in a rootfs, because it's just Linux.

That combination — hardware isolation plus a full, boring OS — is exactly what running untrusted, arbitrary code requires. An AI agent can fork a shell, pip-install a package, spawn subprocesses, and write files, all 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; it makes the guest disposable and the wall around it strong.

Side by side

  • What it is — Firecracker: a minimal KVM VMM running a full Linux guest kernel. Unikernels: app + minimal library OS compiled into one bootable image, no separate kernel.
  • Isolation model — Firecracker: hardware virtualization is the boundary; the guest is a normal, isolated Linux VM. Unikernels: hypervisor-isolated from the host, but a single address space internally (no app-vs-OS memory protection).
  • Attack surface — Firecracker: small VMM boundary, but a full Linux userland inside. Unikernels: minimal — often no shell, no exec, no second process to attack. Verify against each project's docs.
  • Runs arbitrary/untrusted code — Firecracker: yes, anything you can put in a rootfs (its whole reason to exist). Unikernels: no — the image is compiled around one known program.
  • POSIX / fork / exec / shell — Firecracker: full Linux, all present. Unikernels: partial to absent depending on the project (Nanos/OSv/Mirage differ widely).
  • Boot & footprint — Firecracker: ms-scale boot, small per-guest overhead, full-OS image. Unikernels: often smaller images and faster boots (little to initialize).
  • Debugging & ecosystem — Firecracker: standard Linux tooling, SSH, APM agents all work. Unikernels: serial-console-first, thin ecosystem, narrower driver support.
  • Best fit — Firecracker: dense, multi-tenant, untrusted or AI-generated code. Unikernels: a single trusted binary shipped as a minimal appliance.

They're not always even rivals

Worth noting: this isn't strictly either/or at the hypervisor level. NanoVMs and OSv images can themselves boot on Firecracker (and on QEMU) — the unikernel is the guest, Firecracker is the VMM. So 'Firecracker vs NanoVMs' is partly a category error, the same way 'Firecracker vs Docker' is: one is a monitor, the other is a way of building the thing it runs. The real, decision-shaping question isn't which hypervisor — it's which guest model. Do you want a purpose-built single-image appliance, or a general-purpose Linux box you can run anything in?

What driving Firecracker looks like

Because the guest is ordinary Linux, launching a Firecracker microVM is just a small JSON document — a machine config, a boot source, and a root drive — pointing at a normal kernel and a normal rootfs. No app baked into the image, no recompile to change what runs inside.

{
  "boot-source": {
    "kernel_image_path": "./vmlinux",
    "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
  },
  "drives": [
    {
      "drive_id": "rootfs",
      "path_on_host": "./rootfs.ext4",
      "is_root_device": true,
      "is_read_only": false
    }
  ],
  "machine-config": {
    "vcpu_count": 2,
    "mem_size_mib": 1024
  }
}

And because it's a full OS inside, you drive it like a real machine. On PandaStack — an Apache-2.0 platform where every sandbox is its own Firecracker microVM — that means creating a sandbox and running whatever you want in it, including code you'd never compile into a unikernel because you didn't write it.

from pandastack import PandaStack

ps = PandaStack()

# A general-purpose Linux guest, not a single-app appliance.
sb = ps.sandboxes.create(template="base", ttl_seconds=900)

# Run arbitrary code — the exact thing a unikernel can't do:
# a shell, a subprocess, a package install, all inside one isolated VM.
print(sb.exec("echo 'a real shell exists here' && python3 -c 'print(2 ** 10)'").stdout)
print(sb.exec("pip install --quiet requests && python3 -c 'import requests; print(requests.__version__)'").stdout)

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 security 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 surface over the flexibility of a full OS, unikernels deliver something a full Linux guest can't match in sheer economy. That's a real, legitimate win — for the right shape of workload.

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 OS the unikernel throws away — 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's exactly what a microVM gives you.

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 is 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 in 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 there's no shame in a smaller image.

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 a single bootable image that runs in one address space with no separate kernel, no processes, and often no shell. A Firecracker microVM runs a full, normal Linux guest kernel inside a tiny hardware-isolated VMM. Unikernels are small by deleting the OS; Firecracker is small by shrinking the emulator around a normal OS.

Can a unikernel run arbitrary or untrusted code?

Generally no. A unikernel image is compiled around one known program and typically lacks a full POSIX environment — no arbitrary exec, often no way to spawn a second process, no shell by default. Running user-uploaded scripts or AI-agent-generated commands expects a full OS, which is exactly what a unikernel gives up to be minimal. For untrusted code, a full-OS microVM like Firecracker is the fit. Verify each project's current capabilities against its own docs.

Are unikernels more secure than a Linux VM?

They have a smaller attack surface — no shell, no exec, no user model, minimal code — which removes whole categories of exploitation. But they run in a single address space, so there's no memory protection between the app and the library OS; a memory-safety bug that a real kernel would contain to one process can corrupt the whole image. Firecracker keeps a full multi-process OS but wraps it in hardware isolation, so the trade-offs differ. Neither is universally 'more secure' — it depends on the workload.

Can NanoVMs or OSv unikernels run on Firecracker?

Yes — unikernels are guests and Firecracker is a VMM, so NanoVMs/Nanos and OSv images can boot on Firecracker (and on QEMU). That's why 'Firecracker vs NanoVMs' is partly a category mismatch: the real decision is which guest model you want — a purpose-built single-image appliance or a general-purpose Linux box you can run anything in. Check current compatibility in each project's documentation.

When should I use a unikernel instead of Firecracker?

Use a unikernel when you have a single, trusted binary you want to ship as a minimal appliance and value a tiny image and tiny attack surface over flexibility — edge functions, network appliances, latency-sensitive services you own end to end. Use Firecracker when you need to run arbitrary, multi-tenant, or untrusted code that expects a full OS, behind hardware isolation.

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.