InterviewKata
System design · Technology guide T05 · Builds on Modules 08 and 27

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.

A 3-minute conversation: Sam asks, Priya explains
0:00 / 2:32

1. Four ways to run code

Each step down the table hands more of the work to the provider, and takes away some control.

ModelYou manageScalesYou pay forExamples
Virtual machinesThe OS, patching, the runtime, your appBy adding VMs (minutes)Every hour each VM runsEC2, Compute Engine, Azure VMs
KubernetesYour containers and their configuration; the cluster unless managedPods in seconds, nodes in minutesThe nodes (or pod resources on Autopilot-style modes)EKS, GKE, AKS
Serverless containersA container imageInstances in seconds, often to zeroCPU and memory while instances runCloud Run, AWS Fargate, Azure Container Apps
FunctionsA function's codePer request, to zeroExecution time and requestsLambda, 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

VMs · every cloud

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).

Kubernetes · managed

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).

Serverless containers · Google Cloud

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.

Serverless containers · AWS

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.

Serverless containers · Azure

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.

Functions · AWS

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.

Functions · Google Cloud and Azure

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.

Edge · global

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

OptionUnitRequests per instanceLongest requestScale to zeroCold start
VMsA machineAs many as your server handlesUnlimitedNoMinutes to add a VM
KubernetesA podAs many as your server handlesUnlimitedNot by defaultSeconds for a pod; minutes for a node
Cloud RunA container instanceUp to 1,000 (you set it)60 minutesYesTypically under a few seconds
FargateA taskAs many as your server handlesUnlimitedNoTens of seconds for a new task
Container AppsA replicaAs many as your server handlesLong-running allowedYesSeconds
LambdaAn execution environment1 at a time15 minutesYesShort; longer for big runtimes and VPC setups
Edge functionsAn isolateManyShort CPU budgetsYesVery short

5. When to choose which, and why

  1. 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.
  2. 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.
  3. Many services, a platform team, special needs (GPUs, sidecars, long-lived connections, custom scheduling)? Kubernetes.
  4. Need the whole machine (a database you run, licensed software, kernel tuning)? VMs.
  5. 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

Two-sentence answers
  • "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

Kubernetes for one small team's three servicesThe platform costs more attention than the product. Serverless containers do the same job with nothing to operate.
Functions for steady heavy trafficPer-request billing at constant high load usually costs more than always-on instances.
Long work in a functionLambda stops at 15 minutes; move long tasks to a job, a container or a workflow engine.
Ignoring concurrency limitsLambda's default 1,000 concurrent executions per region is shared by all functions; a spike in one can throttle the others. Reserve concurrency for the critical ones.
State on the instanceServerless instances come and go; sessions and files belong in a database, cache or object store.

9. Check yourself

A Lambda function handles 300 requests a second, each taking 0.5 seconds. How many execution environments run at once?

Little's law: 300 per second × 0.5 seconds = 150 in flight, one per environment.

A video transcode takes 40 minutes. Which is a poor fit?

Lambda invocations stop at 15 minutes.

Traffic is high and steady 24 hours a day. What usually costs least?

Scale-to-zero saves money only when there are idle periods.

Why does a Cloud Run instance often need fewer instances than Lambda needs environments for the same traffic?

Lambda runs one request per environment; Cloud Run allows up to 1,000 concurrent requests per instance.

10. Sources and further reading

Product facts checked on 3 November 2026. Limits and features change; check before relying on them.

SourceUsed for
Lambda quotasTimeout, memory, concurrency, payloads, Managed Instances, durable functions
Cloud Run quotas and limitsRequest timeout, concurrency, instance size, job timeout
Module 08: Load balancing and scalingAutoscaling and statelessness
Module 27: Deployment and multi-regionRollouts across the compute options