Learn / Infrastructure and deployment
Serverless vs containers vs VMs
Lesson 23 of 37 · 8 min read ·
The three models
VMs — you rent a machine. You own the OS, patching, capacity planning and scaling. Maximum control, maximum operational load.
Containers — you package the app and its dependencies; a platform (Kubernetes, ECS, Cloud Run, App Service) runs it. You own the image and the config, not the OS.
Serverless functions — you hand over a function. The platform runs it on demand and bills per invocation and per millisecond. You own almost no infrastructure and almost no control.
There is also the middle ground that most people should actually use: managed container platforms — Cloud Run, Fargate, Container Apps, Render, Fly.io. Containers without a cluster to operate. Scale-to-zero, no node management, and no Kubernetes.
What each is genuinely good at
Serverless wins on spiky or infrequent workloads: webhook handlers, scheduled jobs, image processing on upload, glue between services. If your traffic is 50 requests an hour, paying for a machine that idles 99% of the time is waste. Scaling to zero is a real feature, not a marketing one.
Serverless struggles with:
- Cold starts. An idle function must initialise before it responds. Tens of milliseconds for a small JavaScript function, seconds for a large JVM one. Provisioned concurrency fixes it and removes the cost advantage.
- Execution limits. Typically 15 minutes maximum. Long jobs need a different home.
- Database connections. Each concurrent invocation may open its own connection; 500 concurrent invocations will exhaust Postgres. You need a connection pooler (PgBouncer, RDS Proxy) or an HTTP-based database. This surprises people constantly.
- Local development and debugging. Genuinely worse than running a container.
- Vendor lock-in. The function is portable; the twelve pieces of platform glue around it are not.
Containers win as the default for a long-running service: predictable performance, no cold starts, the same image runs locally and in production, and portability between clouds. On a managed platform the operational cost is now low enough that this is the right default for most backends.
VMs win when you need something the platforms will not give you: a specific kernel, GPU work with particular drivers, licensing tied to a host, an unusually large single machine, or a legacy application that will not containerise. Also, occasionally, cost — a reserved VM running steadily is cheaper than the equivalent managed capacity.
Where the cost curves cross
Serverless is far cheaper at low and spiky volume, and more expensive at sustained high volume. The crossover is real: a function handling constant traffic can cost several times what a small always-on container costs.
The mistake is comparing only the compute bill. Include the engineer-hours: a Kubernetes cluster nobody has time to operate is expensive in a way that does not appear on the invoice.
The question that decides it
Which of these can your team actually operate at 3 a.m.?
A three-person team running a self-managed Kubernetes cluster has chosen a second full-time job. The same team on a managed container platform ships features instead. Operational capacity is a real constraint and should be weighted like one.
Sensible defaults for 2026
- Web app or API with steady traffic → container on a managed platform.
- Event-driven glue, webhooks, cron → functions.
- Long-running or GPU work → VMs or dedicated compute.
- Self-managed Kubernetes → only with a platform team, or a genuine multi-cluster need.
Mixing is normal and correct: the API in a container, the image-resize on upload as a function.
Prove you know it
Take something you run and estimate its monthly cost in all three models at current traffic, then at ten times current traffic. The point is not the exact numbers — it is noticing where the curves cross and whether you are on the wrong side of it.