Compute: VMs, containers and serverless
Every design has boxes labelled "service", and sooner or later someone asks what they run on. Virtual machines, Kubernetes, serverless containers and functions all run your code; they differ in how they scale, what you pay for while nothing happens, how long a request may take, and how much of the platform your team must operate. This guide explains each, how the clouds' versions compare, and which to choose, with a chooser and code you can run.
- Sam What do the boxes labelled "service" actually run on?
- Priya Virtual machines, Kubernetes, serverless containers or functions. They all run your code. They differ in how they scale, what you pay while nothing happens, how long a request may take, and how much your team has to operate.
- Sam How do the four compare?
- Priya With VMs you manage the operating system and pay for every hour. Kubernetes runs containers on nodes, managed by EKS, GKE or AKS. Serverless containers, like Cloud Run, Fargate and Azure Container Apps, take a container image and scale it for you. And functions, like Lambda and Azure Functions, run a piece of code per event, down to zero.
- Sam How does scaling to zero work?
- Priya The first request after a quiet period waits for a cold start, while the platform starts an instance. Later requests hit the warm instance. A spike adds instances, each with its own cold start. And after a quiet spell, instances stop and you stop paying. Keep a minimum instance on user-facing paths.
- Sam What limits should I know?
- Priya Lambda runs one request per environment, for up to fifteen minutes, with up to about ten gigabytes of memory and a thousand concurrent executions per region by default. Cloud Run lets one instance serve up to a thousand requests at once, for up to sixty minutes each, and its jobs run for up to seven days. Fargate runs containers on AWS without nodes, but doesn't start tasks per request.
- Sam How do I choose?
- Priya A stateless HTTP service: serverless containers. Short handlers for uploads, messages and schedules: functions. Many services and a platform team: Kubernetes. Software that needs a whole machine: VMs. And for steady high traffic all day, always-on capacity is usually cheaper than paying per request.
- Priya Your turn. A transcode takes forty minutes. Why not Lambda?
- Priya Lambda stops at fifteen minutes. Use a job, a container or a workflow.
- Sam And the code?
- Priya One day of traffic run three ways: VMs sized for the peak, containers that follow the traffic, and functions billed only while busy, using Little's law to turn requests per second into requests in flight.
- Sam Common mistakes?
- Priya Kubernetes for one small team. Functions for steady heavy traffic. Long work in a function. Ignoring the shared concurrency limit. And keeping state on instances that come and go.
1. Four ways to run code
Each step down the table hands more of the work to the provider, and takes away some control.
| Model | You manage | Scales | You pay for | Examples |
|---|---|---|---|---|
| Virtual machines | The OS, patching, the runtime, your app | By adding VMs (minutes) | Every hour each VM runs | EC2, Compute Engine, Azure VMs |
| Kubernetes | Your containers and their configuration; the cluster unless managed | Pods in seconds, nodes in minutes | The nodes (or pod resources on Autopilot-style modes) | EKS, GKE, AKS |
| Serverless containers | A container image | Instances in seconds, often to zero | CPU and memory while instances run | Cloud Run, AWS Fargate, Azure Container Apps |
| Functions | A function's code | Per request, to zero | Execution time and requests | Lambda, Cloud Run functions, Azure Functions |
2. How scaling to zero works
The serverless models start instances when requests arrive and stop them when traffic stops. That's what makes them cheap for spiky traffic, and what causes cold starts.
3. The products
EC2, Compute Engine, Azure VMs
A machine with the CPU, memory, disks and network you choose, behind autoscaling groups (managed instance groups on Google Cloud, scale sets on Azure). Spot or preemptible VMs cost much less but can be reclaimed at short notice.
Best for: software that needs the whole machine (databases you run yourself, licensed software, special kernels, GPUs at steady load).
EKS, GKE, AKS
Managed control planes for running containers on nodes you provide. GKE Autopilot (and similar modes) manages the nodes too and bills per pod resource. Kubernetes gives one way to deploy, scale, discover and roll out every service, at the cost of a platform to learn and operate.
Best for: many services and a platform team, or workloads needing control (sidecars, daemons, GPUs, custom scheduling).
Cloud Run
Runs any container that listens for HTTP. Each instance handles up to 1,000 concurrent requests (you choose), requests may run up to 60 minutes, and instances go up to 8 vCPU and 32 GiB. Scales to zero unless you set minimum instances; Cloud Run jobs run batch tasks for up to 7 days.
Best for: most stateless HTTP services on Google Cloud.
AWS Fargate (with ECS or EKS)
Runs containers without managing EC2 instances: you define tasks with CPU and memory, and ECS or EKS schedules them. It doesn't start a task when a request arrives: a service runs the number of tasks you or its autoscaling set, so there's no request-driven scale from zero.
Best for: containers on AWS without running nodes.
Azure Container Apps
Containers on a managed Kubernetes-based platform with event-driven autoscaling (KEDA), scale to zero, and Dapr for service-to-service calls.
Best for: microservices on Azure without running a cluster.
AWS Lambda
One request per execution environment at a time. Up to 15 minutes per invocation, 128 MB to 10,240 MB of memory (CPU scales with memory), 6 MB synchronous request and response payloads, and 1,000 concurrent executions per region by default. AWS has added Lambda Managed Instances (functions on EC2 capacity) and durable functions (multi-step code with checkpoints).
Best for: event handlers (queues, uploads, schedules) and spiky APIs on AWS.
Cloud Run functions, Azure Functions
Google's Cloud Functions are now Cloud Run functions, built on Cloud Run. Azure Functions offers consumption-style plans that scale to zero and premium or dedicated plans that stay warm.
Best for: small event-driven pieces of code on those clouds.
Cloudflare Workers, CloudFront Functions, Lambda@Edge
Small functions that run at CDN locations near users: rewrite requests, check tokens, personalise cached pages. Tight limits on CPU time and on what they can call.
Best for: logic that must run before the request reaches your region.
4. Differences at a glance
| Option | Unit | Requests per instance | Longest request | Scale to zero | Cold start |
|---|---|---|---|---|---|
| VMs | A machine | As many as your server handles | Unlimited | No | Minutes to add a VM |
| Kubernetes | A pod | As many as your server handles | Unlimited | Not by default | Seconds for a pod; minutes for a node |
| Cloud Run | A container instance | Up to 1,000 (you set it) | 60 minutes | Yes | Typically under a few seconds |
| Fargate | A task | As many as your server handles | Unlimited | No | Tens of seconds for a new task |
| Container Apps | A replica | As many as your server handles | Long-running allowed | Yes | Seconds |
| Lambda | An execution environment | 1 at a time | 15 minutes | Yes | Short; longer for big runtimes and VPC setups |
| Edge functions | An isolate | Many | Short CPU budgets | Yes | Very short |
5. When to choose which, and why
- Is it a stateless HTTP service? Start with serverless containers (Cloud Run, Container Apps, or Fargate on AWS). You ship a container, nothing else to run.
- Is it triggered by events (a file upload, a queue message, a schedule) and short? A function is the least code and the least cost.
- Many services, a platform team, special needs (GPUs, sidecars, long-lived connections, custom scheduling)? Kubernetes.
- Need the whole machine (a database you run, licensed software, kernel tuning)? VMs.
- Steady, high traffic all day? Always-on capacity (VMs, Kubernetes, or minimum instances) is usually cheaper than paying per request; scale-to-zero pays off when traffic is spiky or often idle.
Try it: choose for your requirements
6. One day of traffic, three ways, in code
A service's traffic for each hour of a day, at 0.2 seconds per request. Little's law turns requests per second into requests in flight. VMs sized for the peak run all day; autoscaled containers follow the traffic hour by hour; functions run one request per environment, so they need far more environment-hours, but each is billed only while busy and nothing while idle. The program was run in all three languages; each prints the output below it.
import math
# Requests per second in each hour of one day, and how long each request takes.
RPS = [2, 1, 1, 0, 0, 1, 5, 20, 60, 90, 100, 95, 110, 100, 90, 85, 80, 70, 60, 40, 25, 15, 8, 4]
SECONDS_PER_REQUEST = 0.2
PER_INSTANCE = 20 # requests one VM or container instance handles at once
# Little's law: requests in flight = arrival rate x time each one takes.
in_flight = [rps * SECONDS_PER_REQUEST for rps in RPS]
peak = max(in_flight)
print(f"peak in flight: {peak:.0f} requests")
# VMs: sized once for the peak plus 30% headroom, running all day.
vms = math.ceil(peak * 1.3 / PER_INSTANCE)
print(f"VMs sized for peak: {vms} instances x 24 h = {vms * 24} instance-hours")
# Containers that autoscale each hour (minimum one instance), like Kubernetes or Cloud Run with min instances = 1.
containers = sum(max(1, math.ceil(c / PER_INSTANCE)) for c in in_flight)
print(f"autoscaled containers: {containers} instance-hours")
# Functions: one request per execution environment, billed only while running.
busy_seconds = sum(RPS) * 3600 * SECONDS_PER_REQUEST
print(f"functions: {busy_seconds:,.0f} busy seconds = {busy_seconds / 3600:.1f} environment-hours, "
f"peak {math.ceil(peak)} environments at once")
idle = sum(1 for rps in RPS if rps == 0)
print(f"idle hours: {idle}; scale-to-zero pays nothing then, and the next request is a cold start")
// Requests per second in each hour of one day, and how long each request takes.
const RPS = [2, 1, 1, 0, 0, 1, 5, 20, 60, 90, 100, 95, 110, 100, 90, 85, 80, 70, 60, 40, 25, 15, 8, 4];
const SECONDS_PER_REQUEST = 0.2;
const PER_INSTANCE = 20; // requests one VM or container instance handles at once
// Little's law: requests in flight = arrival rate x time each one takes.
const inFlight = RPS.map((rps) => rps * SECONDS_PER_REQUEST);
const peak = Math.max(...inFlight);
console.log(`peak in flight: ${peak.toFixed(0)} requests`);
// VMs: sized once for the peak plus 30% headroom, running all day.
const vms = Math.ceil((peak * 1.3) / PER_INSTANCE);
console.log(`VMs sized for peak: ${vms} instances x 24 h = ${vms * 24} instance-hours`);
// Containers that autoscale each hour (minimum one instance), like Kubernetes or Cloud Run with min instances = 1.
const containers = inFlight.reduce((sum, c) => sum + Math.max(1, Math.ceil(c / PER_INSTANCE)), 0);
console.log(`autoscaled containers: ${containers} instance-hours`);
// Functions: one request per execution environment, billed only while running.
const busySeconds = RPS.reduce((a, b) => a + b, 0) * 3600 * SECONDS_PER_REQUEST;
console.log(`functions: ${Math.round(busySeconds).toLocaleString("en-US")} busy seconds = ${(busySeconds / 3600).toFixed(1)} environment-hours, ` +
`peak ${Math.ceil(peak)} environments at once`);
const idle = RPS.filter((rps) => rps === 0).length;
console.log(`idle hours: ${idle}; scale-to-zero pays nothing then, and the next request is a cold start`);import java.util.Arrays;
import java.util.Locale;
public class Main {
// Requests per second in each hour of one day, and how long each request takes.
static final int[] RPS = {2, 1, 1, 0, 0, 1, 5, 20, 60, 90, 100, 95, 110, 100, 90, 85, 80, 70, 60, 40, 25, 15, 8, 4};
static final double SECONDS_PER_REQUEST = 0.2;
static final int PER_INSTANCE = 20; // requests one VM or container instance handles at once
public static void main(String[] args) {
// Little's law: requests in flight = arrival rate x time each one takes.
double[] inFlight = Arrays.stream(RPS).mapToDouble(rps -> rps * SECONDS_PER_REQUEST).toArray();
double peak = Arrays.stream(inFlight).max().orElse(0);
System.out.printf("peak in flight: %.0f requests%n", peak);
// VMs: sized once for the peak plus 30% headroom, running all day.
int vms = (int) Math.ceil(peak * 1.3 / PER_INSTANCE);
System.out.printf("VMs sized for peak: %d instances x 24 h = %d instance-hours%n", vms, vms * 24);
// Containers that autoscale each hour (minimum one instance), like Kubernetes or Cloud Run with min instances = 1.
int containers = Arrays.stream(inFlight).mapToInt(c -> Math.max(1, (int) Math.ceil(c / PER_INSTANCE))).sum();
System.out.printf("autoscaled containers: %d instance-hours%n", containers);
// Functions: one request per execution environment, billed only while running.
double busySeconds = Arrays.stream(RPS).sum() * 3600 * SECONDS_PER_REQUEST;
System.out.printf(Locale.US, "functions: %,.0f busy seconds = %.1f environment-hours, peak %d environments at once%n",
busySeconds, busySeconds / 3600, (int) Math.ceil(peak));
long idle = Arrays.stream(RPS).filter(rps -> rps == 0).count();
System.out.printf("idle hours: %d; scale-to-zero pays nothing then, and the next request is a cold start%n", idle);
}
}peak in flight: 22 requests
VMs sized for peak: 2 instances x 24 h = 48 instance-hours
autoscaled containers: 25 instance-hours
functions: 764,640 busy seconds = 212.4 environment-hours, peak 22 environments at once
idle hours: 2; scale-to-zero pays nothing then, and the next request is a cold start
7. Say it in the interview
- "What does this service run on?" Stateless containers on Cloud Run (or Fargate, or Kubernetes if we already run a cluster), scaled on concurrency, with a minimum instance or two to avoid cold starts on the user path. Background handlers for queue messages and uploads can be functions.
- "Serverless or Kubernetes?" Serverless when the team is small and services are plain HTTP or event handlers; Kubernetes when there are many services, a platform team, and needs the managed platforms don't meet.
- "What about cold starts?" Keep minimum instances (or provisioned concurrency) on latency-sensitive paths, keep images and runtimes small, and accept cold starts on background work.
8. Common mistakes
9. Check yourself
A Lambda function handles 300 requests a second, each taking 0.5 seconds. How many execution environments run at once?
A video transcode takes 40 minutes. Which is a poor fit?
Traffic is high and steady 24 hours a day. What usually costs least?
Why does a Cloud Run instance often need fewer instances than Lambda needs environments for the same traffic?
10. Sources and further reading
Product facts checked on 3 November 2026. Limits and features change; check before relying on them.
| Source | Used for |
|---|---|
| Lambda quotas | Timeout, memory, concurrency, payloads, Managed Instances, durable functions |
| Cloud Run quotas and limits | Request timeout, concurrency, instance size, job timeout |
| Module 08: Load balancing and scaling | Autoscaling and statelessness |
| Module 27: Deployment and multi-region | Rollouts across the compute options |