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.
The problem
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: application processscheduled by the guest kernel↓
- VM: guest OS kernelown scheduler, page tables, drivers, syscall table↓
- VM: virtual hardware (vCPU, vNIC, vDisk)traps and emulation; virtio paravirtualised devices↓
- VM: hypervisor (KVM / Hyper-V / Xen)second-level address translation, vCPU scheduling↓
- Host kernel + hardwareshared by both stacks↓
- Container: application processscheduled by the host kernel directly↓
- Container: namespaces + cgroups + overlayfsa view and a budget, not a machine↓
- 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.
| Virtual machine | Container | MicroVM / sandboxed runtime | |
|---|---|---|---|
| Isolation boundary | guest kernel + hypervisor device surface | host kernel syscall checks (namespaces, caps, seccomp) | minimal guest kernel + tiny device model (Firecracker) or user-space kernel (gVisor) |
| Isolation strength | strong | moderate — one kernel bug away | strong (Firecracker) / strong-ish (gVisor: host kernel still reachable via a narrowed set) |
| Startup | seconds to tens of seconds | tens of ms | ~100–200 ms |
| Memory overhead | hundreds of MB per guest | near zero beyond the process | single-digit MB (Firecracker) |
| Density per host | tens | hundreds to thousands | hundreds to thousands |
| Kernel choice | any | host’s only | a curated guest kernel (Firecracker) / none of your own (gVisor) |
| Syscall / I/O overhead | device traps, two kernels on the I/O path | none — native syscalls | virtio path (Firecracker) / syscall interception cost (gVisor) |
| Attack surface from inside | virtual devices, hypervisor | entire host syscall table (minus seccomp) | a few virtio devices / the Sentry’s reimplementation |
| Typical use | multi-tenant clouds, different OSes, strong compliance | your own services, CI, dev environments | serverless functions, code sandboxes, untrusted tenants |
The middle: microVMs and user-space kernels
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
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
- ↓
- ↓
- ↓
- ↓
- ↓
- ↓
| Virtual machine | Container | |
|---|---|---|
| Isolation boundary | hardware virtualisation | kernel namespaces — one shared kernel |
| Startup (simulated) | 5–30 s | ~100 ms |
| Memory per instance | 512 MB – GBs | app only |
| Density per host | tens | thousands |
| Kernel choice | any OS | host’s kernel only |
| I/O overhead | VM exits per I/O, ~5–15% | none — native syscalls |
| Attack surface to host | hypervisor + device emulation | the whole syscall API |
| Typical use | multi-tenant cloud, foreign OS | your own services, dev tooling |
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.