The question this answers
Which execution model fits this workload, and what am I giving up by choosing it?
A team must place a workload — knowing its shape, its traffic pattern, its state, its isolation requirements and, crucially, how many people are available to operate whatever they choose.
A defensible placement with its costs stated: startup characteristics, operational surface, isolation strength, scaling behaviour and cost shape, each traceable to a property of the workload.
Four models, compared on what actually differs
The four models are not a maturity ladder and functions are not the "modern" answer. They are four different points on a trade between control and operational surface, and the right one depends far more on the workload and the team than on the technology.
A virtual machine gives you a whole operating system: any runtime, any daemon, custom kernel modules, long-lived processes, and a strong isolation boundary. You own patching, image building and configuration management, and you scale in units of minutes. A container on your own hosts gives you packaging and density with a much weaker isolation boundary and the same host-level obligations. A managed container platform — from a simple task runner up to Kubernetes — takes placement, restarts and rollouts off your hands, and hands you a control plane to understand and upgrade in return. A function platform takes everything below the code, and hands you cold starts, execution limits, enforced statelessness and a concurrency model you do not control.
The single most under-weighted variable in this decision is the *operator*, not the workload. A managed container platform with a two-person team and no prior experience is a different decision from the same platform with a platform team of eight — the software is identical and the expected outcome is not. This is No Cargo-Cult Infrastructure applied to the most consequential choice in the domain.
| Dimension | Virtual machine | Container (self-hosted) | Managed container platform | Functions |
|---|---|---|---|---|
| Time to first request | Tens of seconds to minutes | Seconds | Seconds | Milliseconds warm; hundreds of ms to seconds cold |
| You own from | Guest OS upward | Host OS + image upward | Image upward | Code upward |
| Isolation strength | Strong — separate kernel | Weaker — shared kernel | Weaker — shared kernel, per-tenant nodes | Provider-managed, usually per-tenant sandboxing |
| Long-running processes | Yes, indefinitely | Yes | Yes | No — hard execution ceiling per invocation |
| Custom OS, kernel modules, daemons | Yes | Limited — shares the host kernel | Rarely | No |
| Scaling unit and speed | Instance, minutes | Container, seconds (capacity permitting) | Container, seconds; nodes in minutes | Invocation, instant to the caller |
| Idle cost | Full instance rate | Full host rate | Node rate + control plane | Near zero |
| Cost shape at high steady load | Predictable and usually cheapest per unit | Cheapest per unit at high density | Good, plus control-plane overhead | Often the most expensive — per-invocation adds up |
| Operational surface | OS patching, config management, images | All of that plus a runtime | Control plane, upgrades, scheduling, networking | Almost none — and almost no visibility |
Six questions, in order
The questions below are ordered so that the ones that *eliminate* options come first. A hard execution ceiling rules out functions for a two-hour job regardless of every other consideration, so ask it before asking about cost.
Question one: does the work outlive a request? A batch job, a stream consumer, a WebSocket server or a background poller needs a long-lived process, which removes functions. Question two: does it need control of the operating system — a custom kernel module, a specific kernel version, a licensed daemon, deterministic scheduling? That pushes toward a virtual machine or bare metal. Question three: what does the traffic look like? Genuinely spiky, low-average, event-driven traffic is the case functions were built for; steady traffic is the case they are worst at.
Question four: how strong must the isolation boundary be? Running untrusted or tenant-supplied code across a shared kernel is a materially weaker boundary than a hypervisor — see Containers vs Virtual Machines. Question five: what operational capacity does the team actually have? and question six: what is the cost shape at expected steady load? Notice that cost is last. It is the question people ask first and the one most likely to be wrong, because the cost curves cross: functions are far cheaper at low volume and frequently far more expensive at high steady volume — see Serverless Trade-offs.
- Long-running? Removes functions. Stream consumers, WebSocket servers and multi-hour jobs need a process, not an invocation.
- OS control? Custom kernel, licensed daemon or deterministic latency pushes to a VM or bare metal.
- Spiky and low-average? The strongest case for functions: near-zero idle cost and instant elasticity.
- Untrusted code? A shared kernel is a weaker boundary. Tenant-supplied code wants VM-grade or microVM-grade isolation.
- Operational capacity? The question that most often should change the answer and most often does not get asked.
- Cost shape? Last, because the curves cross — cheap at low volume is frequently expensive at steady high volume.
The same workload, placed twice
Consider a queue-driven media-processing worker: each job takes three to eight minutes of CPU-heavy work, jobs arrive in bursts, losing a job mid-flight is acceptable because it will be redelivered, and there are four engineers.
Placing it on functions fails at question one: the execution ceiling is below the job duration for the long tail. Placing it on an orchestrated cluster fails at question five: four engineers, no prior operational experience, and nothing in this workload requires a scheduler. The workload has one shape — pull, process, acknowledge — with no service discovery, no rolling-update complexity and no inter-service networking.
A managed container service on interruptible capacity, scaled on queue depth, fits every property: containers start in seconds, the work is naturally idempotent and resumable, interruptible capacity is dramatically cheaper for exactly this shape, and the operational surface is a task definition and a scaling rule. The comparison below is not "less YAML is nicer". It is that the second design has no failure modes the workload did not already have.
cluster: managed kubernetes, 2 node groups, 2 environments installed: - ingress controller # nothing here serves HTTP - cert-manager # no certificates needed - metrics server + KEDA # to scale on queue depth - cluster autoscaler # to add nodes for the above workload: Deployment + HPA + ServiceAccount + NetworkPolicy + PodDisruptionBudget + node selectors for CPU-heavy pods new failure modes acquired: unschedulable pods, node pressure evictions, control-plane version upgrades, CNI issues, scaling that depends on three components agreeing operators: 4, none of whom has run a cluster before
service:
image: worker:sha-9f3c1a2 # pinned by digest
cpu: 2, memory: 4Gi
capacity: interruptible # jobs are redelivered
scale_on: queue_depth
min: 0
max: 40
target_per_task: 5 messages
stop_timeout: 600s # longest job + margin
what you own: the image, the scaling rule, the queue
what you do not own: nodes, scheduler, control plane, upgrades
failure modes: the ones the workload already had
(poison message, slow job, capacity reclaimed mid-job
-> all handled by redelivery, which you needed anyway)Both designs process the same queue at the same rate. The first one adds a scheduler to a workload with nothing to schedule, an ingress controller to a workload that serves no HTTP, and four new outage classes to a team of four. If a second workload arrives with genuine placement, discovery and rollout complexity, the cluster becomes a defensible answer — that is the forcing event, and it has not happened yet. See Kubernetes Is Not Always Needed.
Key points
- Four models trade control against operational surface; none is more modern or more correct than the others in the abstract.
- Ask the eliminating questions first: long-running work removes functions, OS control removes everything but a VM, untrusted code demands a stronger boundary than a shared kernel.
- Team operational capacity belongs in the decision, and it is the variable most often omitted and most often decisive.
- Cost is the last question because the curves cross: functions are cheapest at low, spiky volume and frequently the most expensive at steady high volume.
- A managed container service sits between a VM and an orchestrator and is the right answer far more often than either — it is also the one nobody proposes in a design review.
The loop, answered
Every field is required, which is why no lesson here can recommend something without saying what it costs and what simpler thing to consider first.
- • A virtual machine gives a guest kernel a share of a physical host, so you get a full operating system with a strong boundary and a slow start.
- • A container shares the host kernel and isolates with namespaces and cgroups, giving fast starts and high density with a weaker boundary.
- • A managed container platform adds a control loop that places containers on hosts, restarts them, replaces them on rollout and registers them for traffic.
- • A function platform holds no instance for you between invocations: it creates an execution environment on demand, reuses it while traffic continues, and reclaims it when idle — which is exactly what a cold start is.
- • Virtual machine: OS patching, image building, configuration management, and instance replacement — see The VM Lifecycle.
- • Self-hosted containers: everything above plus a runtime, plus whatever supervises the containers.
- • Managed container platform: the image, the task or workload definition, the scaling rule, and — with an orchestrator — the control plane and its upgrade cadence.
- • Functions: the code, its dependencies, its permissions, its memory setting, and the connection problem it creates against a pooled database — see Serverless and Database Connections.
- • In every model: the identity, the network exposure, the observability and the bill.
- • Functions chosen for a job that occasionally exceeds the execution ceiling: it works for months and fails on the largest input, which is the one that mattered.
- • Functions in front of a pooled database: a burst creates hundreds of concurrent execution environments, each opening connections, and the database refuses them all.
- • An orchestrated cluster adopted without a forcing event: outages caused by unschedulable pods, node pressure and control-plane upgrades — failures the workload did not previously have.
- • A virtual machine fleet with drifted, hand-patched hosts, where a replacement no longer matches production because the changes were never in an image — see Mutable Servers and Immutable Images.
- • A container fleet with a large image and a slow start, so autoscaling arrives after the burst it was meant to absorb.
- • Functions scale per invocation and hit account concurrency limits and downstream connection limits, not compute limits.
- • Containers scale in seconds up to the capacity of the underlying hosts, and then in minutes as nodes are added — a two-tier lag that surprises people.
- • Virtual machines scale in minutes, which means bursts must be absorbed by warm headroom rather than by reaction.
- • In every model the real ceiling is usually downstream: database connections, a third-party rate limit, or a provider quota.
- • Isolation strength differs materially: a hypervisor boundary is stronger than a shared-kernel container boundary, which matters most for untrusted or tenant-supplied code.
- • Each model has a different patching obligation, and the ones where you own the guest OS are the ones that quietly go unpatched.
- • Functions have the smallest attack surface you operate and the least visibility into what happened, which is a genuine trade rather than a pure win.
- • Identity is uniform across models and is where most real incidents happen regardless of which you chose — see Least Privilege in Infrastructure.
- • Virtual machines and containers bill for existence; functions bill for invocation and duration, with near-zero idle cost.
- • The curves cross. Below some steady request rate functions are cheaper; above it, provisioned capacity is, often by a large factor.
- • An orchestrated cluster adds a control-plane charge and a node-utilization problem: you pay for whole nodes and schedule fractions onto them.
- • Interruptible capacity is available to VM and container models and not to functions, and it is the largest single discount in the compute space.
- • Time-to-first-request per model — cold start for functions, time-to-ready for containers and VMs. It decides what your scaling can actually respond to.
- • Concurrency and throttling for functions; node utilization and pending-placement counts for orchestrated clusters; per-instance saturation for VMs.
- • Cost per unit of business work, which is the only way to compare models honestly across very different billing shapes.
- • The signal that lies: a function platform's per-invocation duration metric. It excludes cold-start time in some implementations, so the platform looks fast while users wait.
- • A managed application platform (PaaS) above all four, if the workload is an ordinary web service. It removes the entire decision and is right for a surprising number of teams for a surprising number of years.
- • A single virtual machine running the process under a supervisor, for a small steady internal service — no orchestrator, no images to build, no control plane.
- • A managed container service rather than an orchestrator, whenever the answer to "what forced the scheduler?" is unconvincing. This is the most commonly correct and least commonly proposed answer.
- • Mixing models deliberately: a container service for the API and functions for genuinely event-driven glue is coherent, provided the number of distinct models stays small enough to operate.
- • Every step up the abstraction removes operational surface and removes diagnostic reach into exactly the layers you gave away.
- • Functions buy elasticity and near-zero idle cost and charge cold starts, execution ceilings, enforced statelessness and a connection problem.
- • Orchestration buys placement, self-healing and rollout automation and charges a control plane, a new failure vocabulary and a real learning curve.
- • Virtual machines buy control and strong isolation and charge patching, slow scaling and image management.
Which compute model?
What people believe, and what is true
Serverless is cheaper.
It is cheaper for spiky, low-average workloads because idle costs almost nothing. For steady high-volume traffic it is frequently several times the cost of provisioned capacity — see Serverless Trade-offs.
Containers are always better than VMs.
They are lighter and start faster with a weaker isolation boundary. For untrusted code, custom kernels, licensed daemons or deterministic latency, a VM is the correct answer.
Kubernetes is the destination once you outgrow simpler things.
It is the answer to a specific set of problems — many workloads on shared machines, complex placement, per-service rollouts across teams. Most systems never acquire those problems — see Kubernetes Is Not Always Needed.
Go deeper
Overview
Four models: VM, container, managed container platform, functions. Ask what the workload needs — long-running, OS control, isolation, traffic shape — and what the team can operate.
Practical
Run the six questions in order. Eliminate first, prefer last. Write the answer down with the reason, and record what would change it — a workload that becomes steady, or a second workload with real placement complexity.
Advanced
Model the cost curves for your actual request rate and duration before deciding on price; the crossover between per-invocation and provisioned billing is the whole argument and it moves with memory setting and commitment discounts. Then check the downstream limits, because concurrency almost always hits a database or a third-party rate limit before it hits a compute limit.
Internals
The models differ in what boundary the provider uses and when it is created. A VM boundary is a hypervisor and is created once, slowly. A container boundary is namespaces and cgroups on a shared kernel, created in milliseconds. A function platform creates a sandbox per concurrent invocation and reclaims it when idle — so a cold start is boundary creation plus runtime initialization plus your own startup code, which is why your dependency graph is part of your latency budget.