Containersvirtual machinehypervisorcontainerfirecrackergvisor

VM vs Container: Where the Boundary Is

A VM puts a whole guest kernel and virtual hardware between the workload and the host; a container puts only a narrowed view of one shared kernel — the difference decides isolation strength, startup time, density and which kernel you get, and microVMs exist because neither answer was right for running other people’s code.

ConceptualLinux
▶ InteractiveInterview question
Progress

The problem

You need to run a workload you do not fully trust — a customer’s build, an AI agent’s generated code, a tenant’s function — next to other workloads. A container starts in milliseconds but shares the kernel; a VM has its own kernel but starts in seconds and wastes memory. Which boundary is worth its cost, and is there anything in between?

Two stacks side by side

A virtual machine is a guest operating system running on virtual hardware. The hypervisor (KVM in the Linux kernel, Hyper-V, Xen, or a type-2 hypervisor such as VMware Workstation) presents virtual CPUs, virtual memory, a virtual disk and a virtual NIC. The guest kernel manages its own processes, page tables and drivers exactly as on real hardware; the CPU’s virtualisation extensions (VT-x, AMD-V) let guest code run natively while trapping privileged operations, and nested page tables (EPT) translate guest-physical to host-physical addresses with hardware help. A guest syscall never reaches the host kernel; a guest kernel panic is the guest’s problem.

A container is a process tree on the host kernel with namespaces, cgroups and an overlay root (Containers Are Processes With the Kernel’s View Narrowed). There is no second kernel, no virtual hardware and no hypervisor: syscalls go straight to the host kernel, memory is host memory with cgroup accounting, the “disk” is a directory. What isolates the container is the kernel’s own permission checks — and only those.

VM (left column of the interactive) vs container (right column)
  1. VM: application processscheduled by the guest kernel
  2. VM: guest OS kernelown scheduler, page tables, drivers, syscall table
  3. VM: virtual hardware (vCPU, vNIC, vDisk)traps and emulation; virtio paravirtualised devices
  4. VM: hypervisor (KVM / Hyper-V / Xen)second-level address translation, vCPU scheduling
  5. Host kernel + hardwareshared by both stacks
  6. Container: application processscheduled by the host kernel directly
  7. Container: namespaces + cgroups + overlayfsa view and a budget, not a machine
  8. Host kernel + hardwarethe same kernel the process makes syscalls to

The trade-offs

The columns of the matrix fall out of the stacks. Isolation strength is about how much code sits between an exploit and the host: the whole guest kernel plus a hypervisor with a small device surface, versus the host kernel’s syscall checks. Startup time is booting a kernel versus fork+exec. Density is memory: a guest kernel plus its page cache plus idle daemons costs hundreds of MB before the workload runs, and its RAM is hard to reclaim (ballooning helps, slowly); a container costs its own RSS. Kernel choice is the one thing only a VM gives you — a different kernel version, a different OS entirely, custom modules. Overhead on the hot path is small for both on modern hardware, but VM I/O crosses two kernels and one virtual device.

The numbers are order-of-magnitude, not benchmarks, and they move with hardware: a conventional VM boots in seconds to tens of seconds and holds hundreds of MB; a container starts in tens to hundreds of milliseconds and holds only what the process allocates; a microVM (next section) boots in ~100–200 ms with a few MB of overhead.

The three boundaries (order-of-magnitude figures; measure your own)
Virtual machineContainerMicroVM / sandboxed runtime
Isolation boundaryguest kernel + hypervisor device surfacehost kernel syscall checks (namespaces, caps, seccomp)minimal guest kernel + tiny device model (Firecracker) or user-space kernel (gVisor)
Isolation strengthstrongmoderate — one kernel bug awaystrong (Firecracker) / strong-ish (gVisor: host kernel still reachable via a narrowed set)
Startupseconds to tens of secondstens of ms~100–200 ms
Memory overheadhundreds of MB per guestnear zero beyond the processsingle-digit MB (Firecracker)
Density per hosttenshundreds to thousandshundreds to thousands
Kernel choiceanyhost’s onlya curated guest kernel (Firecracker) / none of your own (gVisor)
Syscall / I/O overheaddevice traps, two kernels on the I/O pathnone — native syscallsvirtio path (Firecracker) / syscall interception cost (gVisor)
Attack surface from insidevirtual devices, hypervisorentire host syscall table (minus seccomp)a few virtio devices / the Sentry’s reimplementation
Typical usemulti-tenant clouds, different OSes, strong complianceyour own services, CI, dev environmentsserverless functions, code sandboxes, untrusted tenants

The middle: microVMs and user-space kernels

Linux

Firecracker (AWS, open source) is a stripped-down VMM on KVM: no BIOS, no PCI enumeration, five virtio devices, a minimal guest kernel configuration. It boots a Linux guest in about 125 ms with under 5 MB of VMM overhead and is designed to be started thousands of times per second — it is what runs AWS Lambda and Fargate, and it is the usual answer for “run untrusted code with a real kernel boundary but container-like startup”. Each function or sandbox gets its own guest kernel; a kernel exploit inside lands in a throwaway VM. Kata Containers wraps the same idea in the container interface, so Kubernetes can schedule a pod that is secretly a microVM.

gVisor (Google) takes the other road. Its Sentry is a kernel written in Go that runs in user space and *intercepts* the container’s syscalls (via ptrace or, faster, as a KVM guest), implementing them itself and touching the host kernel through a deliberately small set of about 50 syscalls. There is no guest OS to boot; the container image and workflow are unchanged; startup is close to a container’s. The cost is compatibility (not every syscall or feature is implemented) and syscall-heavy workloads paying an interception tax. It is the sandbox behind Cloud Run and GKE Sandbox, and the usual choice when you want defence in depth without changing how containers are built.

Neither is free. Firecracker pays memory and I/O virtualisation; gVisor pays syscall latency and compatibility. Both exist because the honest answer to “is a container safe enough for hostile code?” is *no*, and the honest answer to “is a full VM cheap enough for a function that runs for 40 ms?” is also *no*.

  • Rule of thumb: your own code → container; your customer’s code → microVM or gVisor; a different OS or kernel → VM.
  • Chrome’s renderer sandbox is a fourth point on the same line: a process with seccomp-bpf, namespaces and dropped capabilities, no VM — cheap and good, not perfect.

Agent sandboxes and the cloud

Conceptual

An AI agent that runs generated code, executes shell commands or installs packages is the textbook untrusted workload: the code was written by a model, possibly under the influence of a prompt injection, and it must not reach the host, other users’ data or the credentials the agent holds (Tool Permissions and Least Privilege, Tool Misuse and Data Exfiltration). “Sandbox” in that domain means one of the boundaries on this page. Most code-execution sandboxes use Firecracker microVMs or gVisor: a fresh kernel-level boundary per session, started in a fraction of a second, discarded afterward, with the network namespace configured to reach only what the tool is allowed to reach. A plain container with --privileged or a mounted Docker socket is not a sandbox; it is the host with extra steps.

Cloud platforms expose the same three tiers under product names. EC2 instances and Compute Engine VMs are hardware VMs on KVM/Nitro. ECS and GKE pods are containers on VMs you (or they) own. Lambda, Fargate and Cloud Run are microVMs or gVisor sandboxes — which is why a cold start is ~100 ms rather than ~10 ms, why you cannot load a kernel module, and why two functions from different accounts can safely share a physical host.

Key points

  • A VM adds a guest kernel and virtual hardware between the workload and the host; a container adds only a narrowed view of the host kernel.
  • Isolation strength, kernel choice and attack surface favour the VM; startup, density and I/O overhead favour the container.
  • The container’s attack surface from inside is the host syscall table; a VM’s is the virtual device model.
  • Firecracker: a minimal VMM on KVM, ~125 ms boot, ~5 MB overhead, a real kernel boundary per sandbox.
  • gVisor: a user-space kernel that intercepts syscalls; container workflow unchanged, compatibility and syscall cost as the price.
  • Your code → container; someone else’s code (including an agent’s) → microVM or gVisor; a different kernel → VM.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why can’t seccomp and capabilities make a container as strong as a VM?

They shrink what a process may ask of the kernel, but every allowed request still runs host-kernel code in host-kernel privilege. A VM interposes a second kernel and a narrow device interface, so a guest exploit has a much smaller target.

Why do VMs take seconds to start?

Firmware, device enumeration, kernel initialisation, init system, daemons — a whole OS boot. Firecracker shows that most of that is removable: drop the firmware, PCI and unused drivers and the boot is ~100 ms.

Why do serverless platforms use microVMs instead of containers?

Because they run code from thousands of unrelated accounts on shared hosts. A container escape would cross accounts; a microVM escape lands in a throwaway guest. Startup time had to be container-like, hence the micro.

Why does an AI agent’s sandbox matter more than a CI runner’s?

The code was authored by a model that may have been manipulated by its input, it often holds credentials, and it runs interactively against live systems. The threat model is closer to a hostile tenant than to a trusted developer.

VM vs container

VM vs container: where is the boundary?
Click a layer. The question is never “which is better” but “what sits between my code and the tenant next door, and what does that cost?”
Virtual machine
startup ≈ 8.00 s (sim., log scale)98
Container
startup ≈ 100 ms (sim., log scale)50
Container › shared host kernel + hardware: Every container makes syscalls directly into this one kernel. That is why it starts in ~100 ms and why a kernel bug breaks all tenants.
Virtual machineContainer
Isolation boundaryhardware virtualisationkernel namespaces — one shared kernel
Startup (simulated)5–30 s~100 ms
Memory per instance512 MB – GBsapp only
Density per hosttensthousands
Kernel choiceany OShost’s kernel only
I/O overheadVM exits per I/O, ~5–15%none — native syscalls
Attack surface to hosthypervisor + device emulationthe whole syscall API
Typical usemulti-tenant cloud, foreign OSyour own services, dev tooling
Startup figures are illustrative (8.00 s vs 100 ms); a real number depends on the image, the disk and what the guest runs at boot. The meter is on a log-like scale so the container bar is visible at all.
Simulated

How it fails

What the failure looks like from inside real software.

  • Running customer or agent-generated code in a plain container on a shared node; one kernel CVE later, every tenant on the node is exposed.
  • Sizing a node for VMs and getting a tenth of the density expected — each guest kernel and page cache is unreclaimable RAM.
  • Moving a syscall-heavy workload (a build, a database) to gVisor and seeing a 2–5× slowdown from syscall interception.
  • Assuming a Docker Desktop container on a laptop is “like production”: it is running in a Linux VM with different kernel, filesystem and network paths.
  • Loading a kernel module or tuning a sysctl inside a container or a serverless function and being surprised it is refused — there is no kernel of your own to change.