Firecracker vs libkrun: a VMM you run vs a VMM you link
Most VMM comparisons are arguments about device models. This one isn't, or at least not mainly. Firecracker and libkrun both give you a small virtio-shaped machine with a Linux kernel in it, and if you squint at a feature matrix they look like near neighbours. But they are not the same kind of object. Firecracker is an executable: you launch it, supervise it, and talk to it over an HTTP API on a Unix socket. libkrun is a dynamic library: you link it into your own program and the VM runs inside your process. Everything downstream — lifecycle, crash blast radius, who owns the sandbox, what "restart the VM" even means — falls out of that one structural fact.
I'm Ajay. I build PandaStack, which runs Firecracker microVMs as a service, so I have an obvious bias and I'd rather declare it than pretend. I'll also say up front that there is a large class of problems where libkrun is straightforwardly the better tool and reaching for Firecracker would be a mistake. If you're building a CLI, a desktop tool, or a single-purpose runtime that wants "run this one thing in a VM" without becoming an infrastructure company, keep reading but expect the conclusion to go against me.
The shape of the thing
Start with the packaging, because it's the whole story. Firecracker ships as a binary. Your orchestrator forks and execs it (usually via the jailer), it opens a Unix socket, and it sits there waiting for instructions. It is a child process with a PID. You can put it in a cgroup, you can send it a signal, you can watch it die and clean up after it, and you keep running when it does.
libkrun ships as a shared library — from the containers project, the same ecosystem as Podman and Buildah. You link against it, call into it, and the VMM machinery becomes part of your executable. There is no separate process to supervise, because the supervisor and the supervised are the same address space. The start call typically doesn't return: your thread becomes the guest's execution loop. The VM's lifetime is your process's lifetime.
That's not a defect on either side. It's the difference between a component designed to be operated and a component designed to be embedded. A library gives you a machine with no operational surface at all; a supervised binary gives you an operational surface precisely so that something else can own it.
Firecracker asks "who is supervising this VM?" and expects a real answer. libkrun answers "you are, implicitly, because it's inside you."
What libkrun actually is
libkrun exists to let a program acquire the ability to run a workload inside a VM with as little ceremony as possible. Not a fleet. Not a general-purpose machine you SSH into. One workload, container-shaped, isolated by hardware virtualization instead of by namespaces. It's built on the same rust-vmm foundations that most modern minimal VMMs share, and it packages the guest kernel alongside it so that a caller doesn't have to go find a vmlinux and build a rootfs image before anything happens.
The configuration model reflects that single-workload intent. Rather than "attach a block device containing an operating system," the usual libkrun flow is closer to "here is a directory on the host, here is the executable to run inside it, here are the arguments and the environment." The root filesystem is commonly surfaced to the guest as a shared filesystem rather than a disk image you had to build — which is exactly what you want if your input is an OCI image someone already pulled, and exactly what you don't want if your input is a baked, immutable, copy-on-write-able block device.
Networking follows the same philosophy: the project is known for user-mode-ish approaches that avoid making the caller provision host network plumbing, including transparent socket handling that lets guest sockets reach the host side without you creating and owning a tap device. Details here vary by flavor and version — verify against the docs — but the intent is consistent: don't make a desktop tool ask for root to create a bridge.
Flavors, and the macOS thing
The most practically important property of libkrun for a lot of readers: it isn't Linux/KVM-only. It supports Apple's Hypervisor.framework on macOS, which is the reason it shows up under tools like krunvm and under Podman's machine on Macs. If your product needs to run a Linux workload in a VM on a developer's MacBook, that is not a small detail — it is the entire ballgame, and Firecracker simply cannot do it.
The project is also built in flavors: different backends and variants compiled for different hypervisors and different security postures, including work in the confidential-computing direction where the guest is protected from the host platform itself. Which flavors exist, what they're called, and what each supports is exactly the kind of thing that changes between releases, so treat this paragraph as a pointer and go read their documentation. What's durable is the pattern: one library, several backends, chosen at build time.
What Firecracker actually is
Firecracker was written at AWS to run Lambda and Fargate, and its design brief was roughly: run untrusted code from millions of strangers, thousands of guests per host, and never let a compromise escape one VM. The device model is therefore deliberately microscopic — virtio-net, virtio-block, virtio-vsock, a serial console, an entropy source, and a keyboard controller that exists mostly to accept a reboot. Devices sit on MMIO. There is no PCI bus, no BIOS, no bootloader, no graphics, no USB. The kernel is loaded directly and starts executing.
Around that sits the operational apparatus that makes it a fleet component. The control plane is a REST API on a Unix domain socket, meant to be driven by a supervising program rather than a human. The jailer wraps the VMM in a chroot with its own namespaces and cgroups and drops privileges before handing over. A seccomp filter restricts the VMM process to the small syscall set it actually needs, so even a compromised VMM is talking to a deliberately narrowed kernel surface. And snapshot-restore is first-class: freeze a booted guest to a memory file plus a state file, then restore that exact machine as many times as you like.
The catch is the one libkrun doesn't have: Firecracker needs Linux with KVM. There is no macOS build and there isn't going to be one. It's a Linux systems component, not a portable library.
The two shapes, in code
Nothing makes this concrete faster than putting the boot sequences side by side. First Firecracker: an external process, configured over HTTP, running under a jail you asked for explicitly.
# =====================================================================
# Firecracker: a process you launch and supervise. The control surface
# is HTTP over a Unix socket, so ANY language can drive it -- and the
# jail is something you asked for on purpose.
# =====================================================================
set -euo pipefail
SOCK=/srv/jail/vm-42/root/run/firecracker.socket
# The jailer sets up chroot + namespaces + cgroups, drops privileges,
# and only then execs the VMM. Seccomp filters the VMM's syscalls.
jailer --id vm-42 --exec-file /usr/bin/firecracker \
--uid 10042 --gid 10042 --chroot-base-dir /srv/jail \
--netns /var/run/netns/ns-vm-42 \
-- --api-sock /run/firecracker.socket &
api() { curl -s --unix-socket "$SOCK" -X PUT "http://localhost$1" \
-H 'Content-Type: application/json' -d "$2"; }
# You bring the kernel. Loaded directly -- no BIOS, no bootloader.
api /boot-source '{"kernel_image_path":"./vmlinux",
"boot_args":"console=ttyS0 reboot=k panic=1 pci=off"}'
# vCPUs and RAM. That is nearly the entire machine description.
api /machine-config '{"vcpu_count":2,"mem_size_mib":1024}'
# Root filesystem is a block image on virtio-blk -- a file YOU built,
# which is also why it can be reflinked, snapshotted, and shared CoW.
api /drives/rootfs '{"drive_id":"rootfs","path_on_host":"./rootfs.ext4",
"is_root_device":true,"is_read_only":false}'
# Networking is a tap device in a netns that you created and own.
api /network-interfaces/eth0 '{"iface_id":"eth0","host_dev_name":"tap0"}'
api /actions '{"action_type":"InstanceStart"}'
# The guest is now a child process with a PID. You can cgroup it,
# SIGKILL it, watch it die -- and keep running when it does.Now libkrun, conceptually. I'm deliberately writing this as shape rather than as a literal API: the real function names and signatures live in the project's header and docs, and I'd rather you read those than trust my memory of a fast-moving C interface. What matters is the structure, and the structure is unmistakable — there is no socket, no child process, and no supervisor, because you are all three.
/* =====================================================================
* libkrun: NOT a process you launch -- a library you link.
*
* SHAPE ONLY. The real entry points, argument order and return
* conventions live in libkrun's own header and documentation.
* Read those before writing a line of real code against it.
* ===================================================================== */
#include <libkrun.h> /* link against the shared library */
int main(void)
{
/* 1. Ask the library for a VM context. No socket is opened, no
* child is forked. This machine will live in YOUR memory. */
int ctx = /* create a context */ 0;
/* 2. Machine config: vCPUs and RAM, set by a function call
* rather than by a PUT to an HTTP endpoint. */
/* configure vcpus + memory on ctx */
/* 3. Root: point at a directory on the host. The guest sees it
* over a shared filesystem -- you never built a disk image,
* which is convenient here and load-bearing later. */
/* set the root path on ctx */
/* 4. The workload: the ONE thing this VM exists to run.
* An executable, its argv, its environment. */
/* set the exec target on ctx */
/* 5. Start. Typically this does not return -- your thread
* becomes the guest's execution loop. The VM's lifetime
* IS this process's lifetime, in both directions. */
/* start and enter */
return 0; /* reached when the guest workload is done */
}Read those two blocks as job descriptions rather than as code. The first one is written for something that already has a scheduler, a database of VM state, and an on-call rotation. The second is written for something that has a `main()` and a user waiting at a terminal.
Blast radius, and who owns the jail
Here's where the packaging difference stops being aesthetic. With Firecracker, the isolation story has two layers you can reason about separately: the guest is confined by hardware virtualization, and the VMM itself is confined by the jailer and seccomp. If a guest somehow breaks the virtualization boundary, the thing it lands in is a chrooted, unprivileged, cgroup-limited, syscall-filtered process that isn't your application. That second layer is not decoration — it is the difference between a bad day and an incident report with a customer list attached.
With an embedded library, that second layer is whatever your process already is. The guest lands in your address space, with your file descriptors, your credentials, and your environment variables — including, if you're unlucky, the ones holding API keys. You can absolutely sandbox your own process: seccomp, a user namespace, a dropped-privilege child, a container. But you have to do it, deliberately, and nothing in the library is going to remind you. Ownership of the jail moved to you the moment you chose `-l` over `exec`.
Crash semantics mirror this exactly. A Firecracker VM crashing is a child process exiting; your supervisor notices, cleans up the netns and the rootfs clone, and schedules a replacement. Your application never blinked. An embedded VM crashing is your program crashing — and conversely, your program crashing takes the VM with it. For a CLI that's the correct behaviour and arguably a feature: when the user hits Ctrl-C, everything should go away. For a host running four thousand tenants, it's an availability model nobody would choose on purpose.
Side by side
The dimensions that actually differ. Anything version-specific below is flagged; go check their docs before architecting on it.
- What you get — Firecracker: a standalone VMM binary you exec and supervise, with a PID and a lifecycle of its own. libkrun: a dynamic library you link, running the VM inside your process's address space.
- Control interface — Firecracker: a REST API over a Unix domain socket, language-agnostic, drivable from any process. libkrun: direct in-process function calls through a C-callable library interface — no socket, no IPC, no serialization.
- Host platforms — Firecracker: Linux with KVM only; no macOS or Windows build exists. libkrun: multiple backends including Apple's Hypervisor.framework on macOS, which is why it underpins krunvm and Podman's machine on Macs. Verify the current backend list in their docs.
- Intended unit of work — Firecracker: a generic guest machine created and destroyed by an orchestrator, thousands per host. libkrun: one container-shaped workload per VM, launched with minimal ceremony by a tool or an application.
- Root filesystem model — Firecracker: a block image on virtio-blk that you built, which is precisely what makes reflink clones and copy-on-write layering possible. libkrun: typically a host directory shared into the guest, so there's no image to build — great for OCI-shaped inputs, less amenable to block-level CoW tricks.
- Networking — Firecracker: you create and own the tap device and network namespace; the VMM just binds to what you made. libkrun: leans toward approaches that avoid making the caller provision host networking, including transparent socket handling. Check their docs for the current options.
- Isolation of the VMM itself — Firecracker: a jailer (chroot, namespaces, cgroups, privilege drop) plus a seccomp filter, shipped as part of the project. libkrun: the VMM's confinement is whatever confinement your process has, because it is your process. You can do this well; you have to do it yourself.
- Snapshot and restore — Firecracker: first-class, production-grade, and the basis for creating VMs without booting them; one baked snapshot restored thousands of times. libkrun: don't assume parity — check the project's current documentation before designing around it.
- Crash blast radius — Firecracker: VMM dies, supervisor reaps it, application survives. libkrun: VMM dies with your process and takes the VM with it — correct for a CLI, unacceptable for a multi-tenant host.
- Confidential computing — Firecracker: not its focus; the security story is minimal surface plus jailer plus seccomp. libkrun: has work in the confidential-computing direction via specific build flavors. Version-specific — verify.
- Best fit — Firecracker: platforms and orchestrators running many untrusted guests per host, especially where snapshot-restore speed and density are the product. libkrun: tools, CLIs, and applications that want one workload in a VM, on Linux or macOS, without operating a VMM fleet.
Boot, and why snapshot-restore is the dividing line
Both are minimal, virtio-oriented, and skip the legacy firmware theatre, so both start a guest kernel quickly by VM standards. I'm not going to publish head-to-head boot numbers, because a fair one depends on your kernel config, your device set, your storage, and your host — and because inventing a number for someone else's project is how blog posts become citations for things that were never true. Benchmark your own workload.
What I will claim as a structural difference is snapshot-restore, because it changes the shape of the problem rather than the size of a number. Firecracker treats snapshotting a booted guest to a memory file plus a state file as a normal production operation, and restoring one as a normal way to create a VM. That means "create a machine" stops being "boot Linux" and becomes "map some memory and resume." On PandaStack, that's the difference between roughly 3 seconds for the very first cold boot of a template and a p50 of 179ms for every create after that (p99 around 203ms, with the restore step itself around 49ms). Same machine, same kernel, radically different economics.
That property also compounds with the block-image root filesystem. Because the guest disk is a file, it can be reflink-cloned in constant time, and because restored memory is mapped copy-on-write, two VMs from one snapshot share pages until one of them writes. That's what makes forking a warm, fully-initialized machine cheap — 400 to 750ms on the same host in our measurements, 1.2 to 3.5s across hosts. A directory-shared root is lovely for developer ergonomics and doesn't give you that particular lever.
The operational model, which is the real decision
Imagine you're placing the four-thousandth VM of the hour onto host seventeen. You need to know that VM's state without asking the code that created it. You need to kill it from a different process than the one that started it. You need per-VM cgroups so one tenant's fork bomb doesn't become everyone's problem, per-VM network namespaces so tenants can't see each other's traffic, and a story for what happens when the box reboots mid-flight. Firecracker's whole external-process-plus-REST-API design exists so a scheduler can own all of that. PandaStack pre-allocates 16,384 /30 subnets per agent host precisely because that kind of bookkeeping wants to live in the orchestrator, not in the VMM.
Now imagine you're shipping a developer tool. A user runs one command. They want a Linux workload isolated from their laptop, on macOS or Linux, with no daemon, no root, no tap devices, and no second process showing up in Activity Monitor. Every single thing in the previous paragraph is overhead you'd have to build and then hide. libkrun deletes that entire category of work by making the VM an implementation detail of your binary. That's not a lesser goal — it's a different one, achieved well.
The failure mode I'd warn about is picking by feature list instead of by shape. Teams occasionally embed a VMM into a long-lived multi-tenant server because the API was pleasant, then discover they've coupled every guest's lifetime to one process, put untrusted guests behind a boundary whose far side holds their credentials, and made "restart the service" mean "kill every customer's workload." The library didn't lie to anyone. It was just never auditioning for that job.
Where each one wins, plainly
Pick libkrun when
- You need to run Linux workloads in a VM on macOS. Firecracker cannot do this at all — not with a flag, not with a patch. This alone decides a lot of projects.
- You're building a CLI, desktop app, or single-purpose runtime where one VM per invocation is the natural unit and the VM should die when the tool exits.
- Your input is an OCI image or a directory, and you don't want to build and maintain kernel and rootfs artifacts as part of your product.
- You want zero operational surface: no daemon, no socket, no supervisor, nothing for a user to configure or an ops team to run.
- You're exploring confidential-computing flavors where the guest should be protected from the host platform. Check the project's docs for current support.
Pick Firecracker when
- A program, not a person, decides that a VM should exist — and something other than that program needs to be able to inspect, throttle, and destroy it.
- You're running untrusted or model-generated code from many tenants on shared hosts, and you want the VMM itself jailed and seccomp-filtered by default rather than by your own diligence.
- Create latency and density are the product, and snapshot-restore is how you get both: boot once, restore thousands of times, share pages copy-on-write.
- You need a crash boundary between the VMM and your control plane, so a dying guest is an event you handle rather than a stack trace in your own process.
- You're on Linux with KVM anyway, and the portability you'd gain from a library is portability you'd never use.
The third answer: don't operate a VMM at all
There's a case neither project serves directly, and it's increasingly the common one. You don't want to embed a VMM and you don't want to run a fleet of them. You want your application to create an isolated Linux machine as an ordinary function call, because a model is about to run a command nobody has read, or a customer just uploaded a build script, or your agent wants to try five fixes in parallel and keep whichever one passes the tests.
That's the layer PandaStack occupies, with Firecracker underneath: snapshot-restore on every create instead of a warm pool of idle VMs you pay for, a pre-allocated network namespace and tap device per sandbox, a copy-on-write rootfs, and vsock into a guest agent. The API is a Python call — the jailer flags, the tap devices, and the Unix socket are our problem.
from pandastack import Sandbox
# No vmlinux, no rootfs image, no jailer flags, no linking a VMM into
# your web server. Each sandbox is a real Firecracker microVM with its
# own kernel, created by restoring a baked snapshot (p50 179ms).
with Sandbox.create(template="code-interpreter", ttl_seconds=600) as sbx:
sbx.filesystem.write(
"/work/solve.py",
b"import json, sys\n"
b"data = json.load(open('/work/input.json'))\n"
b"print(sum(row['amount'] for row in data))\n",
)
sbx.filesystem.write("/work/input.json", open("input.json", "rb").read())
run = sbx.exec("python /work/solve.py", timeout_seconds=60)
if run.exit_code != 0:
raise RuntimeError(run.stderr)
print(run.stdout)
# Freeze this exact machine: deps installed, data staged, warm.
snap = sbx.snapshot()
# Then branch it. A same-host fork lands in 400-750ms, so "try
# five candidate patches at once" is a for-loop, not a design doc.
for patch in candidate_patches:
child = sbx.fork()
child.filesystem.write("/work/fix.diff", patch.encode())
result = child.exec(
"cd /work && git apply fix.diff && pytest -q",
timeout_seconds=300,
)
record(patch, passed=result.exit_code == 0, log=result.stdout)
child.kill()
# Sandbox is destroyed on block exit; the snapshot outlives it.If that's the shape of your problem, the Firecracker-versus-libkrun question was never really the question — you were choosing between running a VMM yourself and having something run it for you. And if instead you're shipping a tool that needs one VM on a developer's Mac, go read libkrun's docs. I'd rather you use the right thing than the thing I sell.
Frequently asked questions
Is libkrun a Firecracker alternative?
Only for some jobs, because they're different kinds of software. Firecracker is a standalone VMM binary you launch, jail, and drive over a REST API on a Unix socket; libkrun is a dynamic library you link into your own program so the VM runs inside your process. If you're building a tool that wants one workload in a VM with no operational surface, libkrun is a genuine and often better alternative. If you're building a platform that schedules thousands of guests across hosts and needs to supervise, throttle, and reap them independently, an embedded library isn't really competing for that role.
Can Firecracker run on macOS?
No. Firecracker requires Linux with KVM, and there is no macOS or Windows build. To run it on a Mac you need a Linux VM with nested virtualization — a Lima or Multipass VM using Apple's Virtualization.framework, for example — and then Firecracker runs inside that. libkrun is different here: it supports Apple's Hypervisor.framework directly, which is exactly why it shows up behind krunvm and Podman's machine on macOS. If macOS support is a hard requirement, that difference decides the question on its own.
Which is better for running untrusted code?
Both give you a hardware virtualization boundary, which already puts them in a different category from containers. The distinction is what sits behind that boundary and who built the second layer of defence. Firecracker ships a jailer that chroots, namespaces, cgroups, and de-privileges the VMM, plus a seccomp filter on the VMM's own syscalls, so a guest that escapes lands somewhere deliberately impoverished. With an embedded library the VMM's confinement is your process's confinement — your file descriptors, your credentials, your environment — unless you sandbox your own process yourself. For dense multi-tenant untrusted workloads, the supervised model with the jail included is the safer default.
Does libkrun support snapshots like Firecracker?
Don't assume parity, and check the project's current documentation before designing around it. What I can say confidently is that snapshot-restore is a first-class, production-grade feature in Firecracker and the basis of creating VMs without booting them: freeze a booted guest to a memory file plus a state file, then restore it repeatedly. On PandaStack that's the difference between about 3 seconds for a template's first cold boot and a p50 of 179ms for every create afterwards. libkrun's design centre is minimal-ceremony single-workload launch rather than mass restore of baked machines, so verify the current state against their docs rather than inferring it.
Why would I embed a VMM instead of running one as a separate process?
Because for a lot of software, the separate process is pure overhead. If you're shipping a CLI or desktop tool, an external VMM means a binary to bundle, a socket to manage, a child process to supervise, cleanup logic for when it outlives you, and something unexplained showing up in the user's process list. Linking a library collapses all of that into function calls, and the VM dying when your tool exits is the behaviour users actually expect. The trade you accept is coupling: the VM's lifetime becomes your process's lifetime in both directions, and the VMM inherits whatever privileges and secrets your process holds. That trade is excellent for a tool and poor for a multi-tenant server.
49ms p50 cold start. Fork, snapshot, and scale to zero.