Firecracker's documented figures are a sub-125ms boot and under 5 MiB of VMM memory overhead, and both are correct and both are measured at a boundary that is not the one you care about. The 125ms is VMM start to guest kernel init, excluding jailer setup, TAP device creation, IP allocation, rootfs attach and your process actually running, which together commonly add 100 to 300ms on a cold path. The under 5 MiB is the VMM process, excluding the guest kernel's own resident memory, the guest page cache, and the host page cache holding your rootfs. libkrun answers a different question: it is an embeddable library you link into your own process rather than a standalone binary you supervise, it runs on macOS via Hypervisor.framework as well as Linux via KVM, and it trades some per-VM density for a much shorter integration path and no jailer to operate. Pick Firecracker for density on Linux hosts where you run hundreds of concurrent VMs, libkrun when the sandbox has to be embedded in a tool that ships to developer machines. Docker is a different security tier entirely, not a faster point on the same curve. Size hosts on memory and TAP devices, not vCPU: the practical ceiling is usually network plumbing and page-cache pressure long before CPU.
Every microVM comparison starts from the same two numbers: Firecracker boots in under 125ms and adds under 5 MiB of memory overhead per instance. Both are documented, both are reproducible, and both are measured at a boundary that has almost nothing to do with the number your capacity plan needs.
The 125ms is VMM process start to the guest kernel reaching init. Your caller does not experience that boundary. They experience an API call that has to set up a jailer, create cgroups and namespaces, create a TAP device, allocate an IP, attach a rootfs, boot the kernel, run guest userspace to your entrypoint, and get a first byte of output back. The 5 MiB is the hypervisor process, not the guest kernel that lives inside it, not the guest's page cache, and not the host page cache holding the rootfs image that all your VMs are faulting in.
This post is the sizing view rather than the product-pick view: where the boot time actually goes, what the memory overhead figure excludes, how Firecracker and libkrun differ structurally rather than by benchmark, and how to plan a host that runs a few hundred concurrent microVMs without discovering the real ceiling in production. If what you want is the decision framework for which sandbox to run agent-generated code in, that is a different question and it is covered in SmolVM vs Firecracker vs Docker.
Firecracker and libkrun are not the same shape of thing
The most common framing error is treating these two as competing implementations of one idea, then arguing about milliseconds. They are different integration models, and the model determines almost everything downstream.
The row that matters most is integration model. Firecracker hands you a process to supervise and a jailer to configure, which is exactly what you want when you are running a fleet: the failure domain is a process the supervisor can kill, restart, and account for. libkrun hands you a function call, which is exactly what you want when you are shipping a CLI or an agent runtime that has to sandbox code on someone else's laptop, and exactly what you do not want when a crash in the VM thread can take down the tool that was supervising it.
The Docker column is in the table to be dismissed on the correct grounds. It is not a faster point on the same curve. It is a different security tier: a shared host kernel means a kernel vulnerability or a runtime escape crosses the boundary, which is a defensible risk for a build cache and an indefensible one for code that arrived from an attacker-influenceable prompt.
| Firecracker | libkrun | Docker | |
|---|---|---|---|
| Integration model | Standalone VMM binary, one process per VM | Library linked into your process | Daemon plus container runtime |
| Isolation boundary | Hardware virtualisation, separate guest kernel | Hardware virtualisation, separate guest kernel | Namespaces and cgroups, shared host kernel |
| Hypervisor backend | KVM only | KVM on Linux, Hypervisor.framework on macOS ARM64 | None |
| Host platforms | Linux, x86_64 and aarch64 | Linux and macOS | Linux, plus a Linux VM on macOS and Windows |
| Process supervision | You supervise the VMM plus its jailer | The VM is a thread in your process | The daemon supervises |
| Device model | Minimal virtio set, deliberately small | Minimal virtio set plus optional passthrough | Full host device access via the kernel |
| Documented overhead | Under 5 MiB VMM memory, sub-125ms boot | Sub-200ms class startup, workload-dependent | Effectively zero VMM overhead |
| Best at | Density on Linux fleets | Embedding a sandbox in a shipped tool | Trusted workloads and cached builds |
Where the boot time actually goes
Break a cold start into four stages and instrument each one. The published figures only cover stage two.
Two things fall out of that decomposition and they are the ones that change engineering decisions.
Stage 1 is usually the largest and it is not the hypervisor's fault. TAP device creation, IP allocation from a pool, and jailer chroot setup are host operations serialised on kernel locks that get contended when you spin up many VMs at once. A control plane that creates a TAP device on the request path will see stage 1 balloon at concurrency in a way that never appears in a single-VM benchmark. The fix is to pre-create network devices into a pool and hand them out, which is unglamorous and frequently cuts more milliseconds than any VMM tuning.
Stage 3 is where sloppy rootfs images hide. A full distribution rootfs running a real init system, spawning a logger, a DHCP client, and an SSH daemon will spend several hundred milliseconds doing work your sandbox does not need. A minimal image whose init is your entrypoint collapses stage 3 to near nothing. Most of the difference between a "microVMs are slow" experience and a "microVMs are fine" experience is here, not in the choice between Firecracker and libkrun.
If you take one operational action from this section: measure your own four stages before you compare runtimes, because there is a good chance the runtime is not your bottleneck.
| Stage | What happens | Typical contribution | Controlled by |
|---|---|---|---|
| 1. Orchestration | Jailer, cgroups, namespaces, TAP device, IP allocation, drive attach | 40 to 200ms cold, and it scales badly under concurrency | Your control plane, not the VMM |
| 2. VMM boot | VMM start to guest kernel init | Sub-125ms documented for Firecracker with a minimal guest | Kernel config, image size, VMM |
| 3. Guest userspace | init to your entrypoint executing | 20 to 400ms, entirely dependent on the rootfs | Your rootfs, your init |
| 4. Workload ready | Entrypoint to first useful output | Whatever your process needs | Your code |
What the 5 MiB memory figure excludes
Firecracker's documented overhead of under 5 MiB per microVM is a statement about the VMM process. It is genuinely impressive and it is genuinely not your per-VM cost. Here is the full accounting for one running microVM:
Three practical consequences.
First, a microVM configured with 512 MiB will eventually occupy something close to 512 MiB of host memory whether or not the workload needs it, because the guest kernel will happily fill unused memory with page cache. Memory ballooning exists to claw that back, and on a dense host it is not optional.
Second, rootfs sharing is a first-order capacity decision. Hundreds of VMs booting from a read-only copy-on-write base image share host page cache for the base. Hundreds of VMs each booting from their own full copy do not, and the host page cache becomes the binding constraint at a fraction of the density you planned for.
Third, the guest kernel is a config decision with a direct memory price. Stripping drivers you will never use in a virtio-only guest reduces both stage 2 boot time and resident kernel memory, and it is the single highest-leverage tuning knob available to someone who controls their own images.
| Component | Where it lives | Typical size |
|---|---|---|
| VMM process overhead | Host, per VM | Under 5 MiB documented for Firecracker |
| Guest kernel resident set | Inside guest memory | 15 to 40 MiB depending on compiled drivers and features |
| Guest userspace | Inside guest memory | Your workload |
| Guest page cache | Inside guest memory, up to the configured ceiling | Grows to fill whatever you allocated |
| Host page cache for rootfs | Host, shared if the image is shared | One copy per distinct image, if you get sharing right |
| Balloon-reclaimed memory | Returned to host, if configured | Negative, and worth configuring |
Sizing profiles: four workloads, four different answers
Density planning fails when one profile is applied to everything. These four cover most of what we see in production.
The ephemeral exec row is the one that has changed most in the last two years, because that is the sandbox shape an AI coding agent needs: start from nothing, run one untrusted thing, exit, keep nothing. Cold start dominates and density is secondary, which is the profile where libkrun's embeddability and cross-platform support tend to beat Firecracker's density. The per-tenant row inverts every one of those weightings, which is why AWS built Lambda and Fargate on Firecracker and not on something embeddable.
If your workload is closer to "give me a hosted sandbox API and stop making me operate this", the managed options are compared in Modal vs E2B vs Daytona vs Vercel Sandbox.
| Profile | Workload | vCPU | Guest memory | Rootfs | Network | Runtime |
|---|---|---|---|---|---|---|
| Ephemeral exec | One agent-generated script, seconds of runtime, then discard | 1 | 128 to 256 MiB | Minimal, read-only, shared base | Egress denied by default, allowlist per task | libkrun if embedded, Firecracker if fleet |
| CI job runner | Test suite or build, minutes, needs a toolchain | 2 to 4 | 2 to 8 GiB | Layered image with cached deps | Registry and package mirror allowlist | Firecracker with a warm pool |
| Per-tenant service | Long-lived, one VM per customer | 1 to 2 | 512 MiB to 2 GiB | Per-tenant persistent volume | Per-tenant network namespace | Firecracker, snapshot-restored |
| Developer laptop | Sandbox inside a CLI or agent, cross-platform | 1 to 2 | 256 MiB to 1 GiB | Bundled with the tool | Loopback plus explicit allowlist | libkrun, for Hypervisor.framework |
Firecracker vs Docker: what the extra 100ms buys
This is the comparison people actually type into a search box, and it deserves a straight answer rather than a hedge.
The honest summary: Docker is faster, lighter, and easier, and none of those advantages address the threat model that makes people ask the question. If the code inside the boundary was written by a language model from a prompt a user controls, a shared kernel is the wrong isolation primitive and the cost of a separate kernel is the price of the correct one. If the code is your own, built in your own CI, running behind your own auth, a container is fine and a microVM is theatre.
There is a middle path worth naming: keeping the untrusted execution off a VM entirely by constraining what the code can reach, which is the approach discussed in sandboxing coding agents locally without a VM. It is weaker isolation and a much lower operational cost, and for a single-developer laptop that trade is often correct.
| Docker | Firecracker | |
|---|---|---|
| Kernel | Shared with host | Separate per VM |
| Escape class | Container escape and host kernel vulnerabilities are in scope | Requires a hypervisor escape, a far smaller and harder surface |
| Cold start | Tens of milliseconds warm, hundreds cold | Sub-125ms VMM boot plus 100 to 300ms of setup |
| Per-instance memory | Near zero beyond the workload | Guest kernel plus VMM overhead, tens of MiB |
| Syscall performance | Native | Native inside the guest, virtio cost at the device boundary |
| Operational surface | Images, registry, daemon | Kernel images, rootfs images, network plumbing, jailer |
| Correct use | Trusted code, cached builds, internal workers behind auth | Untrusted or attacker-influenced code, multi-tenant execution |
Snapshots, and the three things they quietly break
Snapshot and restore is the standard answer to cold start: capture a booted VM's memory and device state, then restore clones from it and skip kernel boot and init entirely. Restore lands well under a fresh boot because it skips the two most expensive stages.
Three inherited problems come with it, and all three have bitten real deployments.
Entropy is cloned. Every VM restored from one snapshot starts with an identical entropy pool. If the guest generates a key, a token, a session identifier, or a nonce shortly after restore, those values can collide across clones. This is a cryptographic failure, not a performance quirk, and it needs an explicit reseed on restore.
Time is frozen. The guest clock reads the snapshot moment until something corrects it. TLS certificate validation, token expiry checks, and anything log-ordered will misbehave until you resynchronise.
Network state is stale. Interfaces need reattaching, and anything the guest cached before the snapshot (DNS answers, connection pools, credentials with an expiry) is now wrong by however long the snapshot has been sitting.
Use snapshots for warm pools of short-lived, identical, non-secret-generating jobs. Do not use them for per-tenant VMs that hold credentials, and never restore a snapshot older than the shortest expiry inside it.
A benchmark harness worth running before you choose
Vendor numbers are correct and unhelpful. Yours will differ because your rootfs, your kernel config, your network plumbing, and your concurrency are different. The harness that answers the question is small:
# Report p50 and p99 per stage, not a mean. Means hide the tail that pages you.
#
# Stage 1 t_orchestrate : API accepted -> VMM process exists
# Stage 2 t_vmm : VMM process exists -> guest kernel init
# Stage 3 t_userspace : guest kernel init -> entrypoint executing
# Stage 4 t_ready : entrypoint -> first useful output
# Run each configuration three ways. The deltas between them are the finding.
# cold : drop host page cache first, nothing shared, nothing prewarmed
# warm : rootfs resident in host page cache, network devices pre-created
# loaded: at your real target concurrency, not one VM at a time
for runtime in firecracker libkrun docker; do
for mode in cold warm loaded; do
for i in $(seq 1 200); do
run_one "$runtime" "$mode" # emit the four stage timestamps as one row
done
done
doneTwo things this harness will tell you that a single-VM benchmark cannot.
The cold versus warm delta is frequently larger than the delta between runtimes. If dropping the host page cache costs you 300ms and switching hypervisors saves you 40ms, you now know where to spend the week.
The loaded row is where TAP device contention, IP pool exhaustion, and page-cache thrash show up. A runtime that looks 20% faster at concurrency 1 and falls apart at concurrency 200 is the wrong choice for a fleet and nobody finds that out from a published number.
Choosing, in one paragraph each
Choose Firecracker when you run a Linux fleet, you want hundreds of concurrent VMs per host, and you can afford to operate rootfs images, kernel images, network plumbing, and a jailer. Its density and its operational track record at very large scale are not seriously contested, and the supervision model (a process you can kill and account for) is the right failure domain for a fleet.
Choose libkrun when the sandbox has to be embedded rather than supervised, or when it has to run on macOS. A developer tool that sandboxes code on the user's own Apple Silicon laptop cannot use Firecracker without shipping a Linux VM to hold it, and the resulting stack is worse than just using the library that already speaks Hypervisor.framework. You give up some density you probably were not going to use.
Choose Docker when the code is trusted. It is faster, lighter, and simpler, and the moment you find yourself writing a paragraph explaining why the shared kernel is acceptable for attacker-influenced code, you have chosen wrong.
At Particula Tech we size and operate this layer on-premise, usually for teams in regulated industries who need the isolation boundary documented as clearly as the throughput number. The capacity plans that survive contact with production are the ones built from a measured density test at the real guest size, with the network plumbing in the measurement. The ones that fail are built by dividing host RAM by guest RAM. The rest of our infrastructure and tooling work is collected on the AI development tools pillar.
FAQ
Quick answers to the questions this post tends to raise.




