System designCore10 min
Compute models: VMs, containers & serverless
Code has to run somewhere: on a virtual machine you manage, in a container packed onto shared machines, or as a function the platform starts for each request. The choice decides how much you operate, how strongly workloads are isolated, how fast new capacity starts, and whether you pay for machines or for work. Most systems end up using more than one.
1 · The choice
Each model trades control for less to manage
An online course platform runs five kinds of work. A web app and API serve students all day. Instructors upload lecture videos that must be transcoded, sometimes for an hour. Payment-provider webhooks arrive in bursts. Every night, certificates are rendered as PDFs. And students submit code that the platform compiles and runs to grade it, code nobody at the company has read.
Each could run on any of three models. They differ in what you manage and what you pay for:
Aspect | Virtual machine | Container | Serverless function |
|---|---|---|---|
You manage | OS, runtime, app, scaling | Image and app; an orchestrator places it | Just the code |
Isolation | Own kernel | Shared kernel | Platform's choice, often a micro-VM |
Starts in | Slowest: a whole OS boots | Faster: a process starts | Fast when warm; a cold start adds delay |
You pay for | Running time, busy or idle | The machines underneath | Each request and its duration |
Under the labels, all three are the same thing: a Linux process running on a physical server. What changes is how much is wrapped around it, who builds the wrapping, and how billing is measured. Seeing each wrapping makes the trade-offs predictable.
2 · Virtual machines
A virtual machine is a whole computer, with its own kernel
A virtual machine (VM) is an emulated computer run by a hypervisor on a physical host. Each VM boots its own operating system with its own kernel, so a crash or compromise inside one doesn't touch its neighbors except through the hypervisor. That is the strongest isolation of the three, and the traditional way to run different customers' code on the same hardware.
Each VM boots a whole operating system. A crash inside one reaches its neighbors only through the hypervisor.
The price is weight. Each VM carries a full operating system, so it uses more memory and starts slower than a process: a whole OS has to boot before your code runs. And you pay for it while it runs, busy or idle; EC2, for example, bills a running Linux instance per second with a one-minute minimum each time it starts.
For the course platform, VMs suit the steady web app if the team wants full control of the machine, and anything that needs a custom kernel, special hardware such as GPUs for transcoding, or software licensed per machine. Patching the OS, scaling the fleet and replacing failed machines are the team's job (Autoscaling).
3 · Containers
A container is an isolated process sharing the host's kernel
A container is not a small VM. It is an ordinary process that the Linux kernel isolates using two features:
- Namespaces give it its own view of global resources: its own process IDs, network interfaces, mount points and hostname.
- Control groups (cgroups) limit and account for what it may use: CPU, memory, I/O.
It starts from an image holding its files and dependencies, so it runs the same on a laptop and in production. All containers on a host share one kernel, which is why many can run on less hardware than the same number of VMs.
Each container sees only its own processes, network and files, but every system call goes to the same kernel. That shared kernel is both the efficiency and the risk.
An orchestrator such as Kubernetes decides where containers run. Each container declares a request, what it needs, which the scheduler uses to pick a machine with room, and a limit, which the kernel enforces through cgroups. The two limits behave differently: a container at its CPU limit is throttled, slowed down; a container over its memory limit can be killed by the kernel's out-of-memory killer. Set memory limits with headroom.
The shared kernel is the catch. The traditional view is that VMs give strong security at high overhead and containers weaker security at minimal overhead, so containers alone are the weaker choice for code you don't trust, such as the students' submissions. Two ways to harden them: gVisor runs containers on an application kernel written in Go in user space, so the code never talks to the host kernel directly; or each container gets its own micro-VM, later on this page.
4 · Serverless
A function runs per request, and the first request may wait for a cold start
A serverless function, such as AWS Lambda, is code the platform runs when an event arrives: an HTTP request, a queue message, a file upload. There are no servers to size or patch. The platform creates execution environments as needed and bills for each request and for its duration, in milliseconds, scaled by the memory configured.
The webhook handler is a natural fit: idle most of the time, a burst when a sale ends. Lambda gives each concurrent request its own execution environment, so the number it needs is concurrency = requests per second × average duration. At 40 webhooks a second taking 0.2 seconds each, that is 8 environments. The first request into a new environment pays for setting it up:
Cold starts hit under 1% of Lambda invocations, lasting from under 100 ms to over a second, and the setup time is billed. A reused environment keeps its database connections and loaded libraries.
The model has firm edges. A Lambda invocation runs at most 15 minutes and can use up to 10,240 MB of memory, and by default an account may run 1,000 executions at once, a quota that can be raised. Hour-long video transcoding doesn't fit. For latency-sensitive paths, provisioned concurrency keeps a set number of environments initialized in advance, at a price, which trades away part of the pay-per-use model.
Serverless containers, such as Cloud Run, sit in between: you supply a container image, the platform scales it, and it can scale to zero. Unlike Lambda, one instance can serve many requests at once, up to 1,000, by default 80 per vCPU when deployed from the command line, so a burst needs fewer cold starts. A minimum number of instances can be kept warm.
5 · Micro-VMs
Micro-VMs give each workload its own kernel at container speed
Public serverless platforms face the course platform's grading problem at enormous scale: running strangers' code side by side. Containers alone were too weak and classic VMs too heavy, so AWS built Firecracker, a minimal virtual machine monitor. Each workload gets a micro-VM with its own small Linux kernel:
Firecracker micro-VM | Figure |
|---|---|
Memory overhead | Under 5 MB per micro-VM |
Time to application code | Under 125 ms |
Creation rate | Up to 150 micro-VMs per second per host |
In production | Lambda since 2018, and Fargate |
So the isolation column in the first table is less fixed than it looks: a "serverless function" may run inside a VM-strength boundary, and a "container" can be given one too. For the grading sandbox, the course platform can run each submission in a micro-VM or under gVisor, with a CPU and time limit, and no network access.
6 · The cost
Paying per request wins at low and spiky load, and loses when busy all day
Functions bill for work done; machines bill for time running. Which is cheaper depends on how busy the machine would be. Take the webhook handler at 512 MB and 0.2 seconds per request. At the x86 rate in Lambda's pricing examples, $0.0000166667 per GB-second plus $0.20 per million requests, each request costs 0.5 × 0.2 × $0.0000166667 + $0.0000002 ≈ $0.0000018667. Compare that with a small always-on server, assumed here to cost $50 a month:
Monthly function cost = requests × $0.0000018667. It passes the $50 server at about 26.8 million requests a month, an average of about 10 a second.
The crossover moves with every assumption: longer requests, more memory or a cheaper server pull it lower; spikier traffic favors the function, because a server must be sized for the peak while the function pays only for what runs. The arithmetic also leaves out what doesn't appear on the bill: patching, scaling and on-call for the server, and cold starts and platform limits for the function.
7 · In practice
Match each workload to the model that fits its shape
Course platform workload | Shape | Fits |
|---|---|---|
Web app and API | Steady all day, latency-sensitive | Containers on an orchestrator, or VMs |
Video transcoding | Bursty, up to an hour, wants GPUs | Containers or VMs on a job queue, scaled to the queue |
Payment webhooks | Idle, then bursts; short | Functions |
Nightly certificates | Once a day, minutes long | A scheduled container job or function |
Grading student code | Untrusted, short | Micro-VMs or gVisor, strict limits |
For any workload, the checklist:
- Start from the shape: steady or spiky, seconds or hours, trusted or not.
- Prefer the least you have to operate that meets the latency, duration and isolation the workload needs.
- Keep services stateless, so any model can add and remove copies (Stateless services & sessions).
- Set container requests and limits, with memory headroom.
- Watch function limits: duration, memory, concurrency, and cold starts on user-facing paths.
- Run untrusted code behind a kernel boundary you don't share.
Check yourself
5 questions
Takeaways
Remember this
- VMs, containers and functions differ in what you manage, how strongly workloads are isolated, how fast capacity starts, and what you pay for.
- A container is an isolated process sharing the host kernel; namespaces isolate it and cgroups limit it.
- Functions bill per request and duration, start an environment per concurrent request, and have hard limits on duration, memory and concurrency.
- Micro-VMs such as Firecracker give each workload its own kernel with container-like overhead, which is how serverless platforms run strangers' code.
- Per-request pricing wins at low and spiky load; always-on machines win when busy all day, before counting the operations work each needs.