Skip to main content
Cloud Interviews beginner Lesson 2 of 10

VM, Container, or Serverless

The break-even calculation that decides between them, why serverless gets expensive at steady load, cold starts measured, and the questions each choice invites.

There is no universally right answer, and interviewers know it. What is being scored is whether you asked about the traffic shape and then did the arithmetic.

The break-even, computed

# breakeven.py
HOURS = 730

# a VM sized to handle the peak, running all month
VM_HOURLY = 0.0832                    # m7g.large on demand
VM_MONTHLY = VM_HOURLY * HOURS

# serverless: per-invocation fee plus memory-time
INVOKE_COST = 0.20 / 1_000_000        # per request
GB_SECOND   = 0.0000166667            # per GB-second
MEMORY_GB   = 0.5
DURATION_S  = 0.150

def serverless_monthly(requests):
    return requests * (INVOKE_COST + MEMORY_GB * DURATION_S * GB_SECOND)

print(f"VM (always on):      ${VM_MONTHLY:>10,.2f}/month\n")
print(f"{'requests/month':>16} {'serverless':>12} {'vs VM':>10} {'avg RPS':>9} {'util':>7}")
for requests in (100_000, 1_000_000, 10_000_000, 50_000_000, 100_000_000, 500_000_000):
    cost = serverless_monthly(requests)
    rps  = requests / (HOURS * 3600)
    util = requests * DURATION_S / (HOURS * 3600)      # fraction of a single core busy
    flag = "cheaper" if cost < VM_MONTHLY else "MORE"
    print(f"{requests:>16,} {cost:>11,.2f} {flag:>10} {rps:>9,.1f} {util:>6.1%}")
$ python breakeven.py
VM (always on):      $     60.74/month

  requests/month   serverless      vs VM   avg RPS    util
         100,000        0.145    cheaper       0.0    0.6%
       1,000,000        1.450    cheaper       0.4    5.7%
      10,000,000       14.500    cheaper       3.8   57.1%
      50,000,000       72.500       MORE      19.0  285.4%
     100,000,000      145.000       MORE      38.1  570.8%
     500,000,000      725.000       MORE     190.3 2854.1%

The crossover is around 42 million requests a month — roughly 16 requests a second sustained, or about 240% of one core busy.

Note the util column past 100%: at that point a single VM cannot serve the load either, so the comparison needs more VMs and the crossover moves right. The honest version:

VMS_NEEDED = lambda util: max(1, math.ceil(util * 1.4))    # 1.4 = headroom for peaks
     50,000,000       72.50   vs  4 VMs at $242.94   → serverless still cheaper
    100,000,000      145.00   vs  8 VMs at $485.89   → serverless still cheaper
    500,000,000      725.00   vs 40 VMs at $2,429.44 → serverless still cheaper

Which reverses the conclusion. At 150 ms and 0.5 GB, serverless stays cheaper than on-demand VMs at every volume here — because the VM is being charged for peak capacity it uses at only 70% average.

“The break-even depends almost entirely on two numbers: execution duration and how spiky the traffic is. At 150 ms, serverless wins for a long way. At 2 seconds of CPU-bound work per request, it flips early. And this compares against on-demand — a three-year reserved instance is about 60% cheaper, which moves the line substantially. I’d want the duration measured before committing either way.”

That paragraph is the answer. The number is not.

The variables that actually move it

# sensitivity.py
def crossover_requests(duration_s, memory_gb, vm_monthly):
    per_request = 0.20/1_000_000 + memory_gb * duration_s * 0.0000166667
    return vm_monthly / per_request

VM_ONDEMAND = 60.74
VM_RESERVED = 24.30        # ~60% off, 3-year commitment

print(f"{'duration':>10} {'memory':>8} {'vs on-demand':>16} {'vs reserved':>14}")
for d in (0.05, 0.15, 0.5, 2.0):
    for m in (0.25, 0.5, 2.0):
        a = crossover_requests(d, m, VM_ONDEMAND)
        b = crossover_requests(d, m, VM_RESERVED)
        print(f"{d:>9.2f}s {m:>7.2f}G {a:>16,.0f} {b:>14,.0f}")
$ python sensitivity.py
  duration   memory     vs on-demand    vs reserved
     0.05s    0.25G      148,750,318     59,510,684
     0.05s    0.50G       98,497,111     39,405,288
     0.05s    2.00G       32,539,286     13,017,857
     0.15s    0.25G       73,624,172     29,454,145
     0.15s    0.50G       41,889,588     16,758,580
     0.15s    2.00G       11,680,738      4,673,095
     2.00s    0.25G        7,117,576      2,847,730
     2.00s    0.50G        3,601,027      1,440,727
     2.00s    2.00G          908,373        363,417

A 164x spread between the top row and the bottom. Duration and memory decide this, not the choice of platform, and a reserved instance moves every crossover left by 60%.

The interview-usable summary:

Short (< 200 ms), low memory, spiky      → serverless, comfortably
Long (> 1 s), high memory, steady        → VMs or containers, comfortably
Anything in between                      → do the arithmetic; it is genuinely close
Unpredictable spikes                     → serverless, for the elasticity not the cost
Steady 24/7 at high utilisation          → reserved instances, and it is not close

Cold starts, measured

# invoke a cold function, then the same one warm
aws lambda invoke --function-name demo-python --payload '{}' /dev/null \
  --cli-read-timeout 60 --query 'ExecutedVersion' --output text
REPORT Duration: 1.42 ms  Billed Duration: 2 ms  Memory Size: 512 MB
       Max Memory Used: 42 MB  Init Duration: 118.71 ms      ← cold
REPORT Duration: 1.38 ms  Billed Duration: 2 ms  Memory Size: 512 MB
       Max Memory Used: 42 MB                                ← warm, no Init line

Init Duration appears only on a cold start. Typical ranges, worth carrying as anchors:

RUNTIME                          COLD START     NOTES
Python / Node, small deps        100-300 ms     the common case
Python with numpy/pandas         800-2,000 ms   import time dominates
Go / Rust compiled binary        50-150 ms      no runtime to boot
JVM, Spring Boot                 3,000-8,000 ms often disqualifying
Any of the above in a VPC        +0-100 ms      much worse before ENI pre-warming
Container image, large           1,000-4,000 ms image pull and unpack

The mitigations, and what each costs:

  • Provisioned concurrency — keeps N instances warm. It works, and it reintroduces the always-on charge you moved to serverless to avoid. Costing it out is the right response when someone proposes it.
  • Smaller deployment package — the highest-leverage fix for the interpreted runtimes, since import time dominates.
  • Move initialisation outside the handler so it runs once per sandbox, not once per request.
  • Accept it. For an asynchronous job, a 2-second cold start is invisible. Cold starts only matter on a user-facing synchronous path.

“Cold starts matter on a synchronous user-facing path and usually do not matter for asynchronous work. Before I design around them I’d ask what the latency budget is — if p99 under 200 ms is a requirement, a JVM Lambda is disqualified regardless of cost.”

The three platforms, honestly

                 VM / ASG          CONTAINERS           SERVERLESS
control          full OS access    image + runtime      function only
scaling unit     instance          task                 request
scale-up time    60-180 s          10-40 s              ~0 (cold start)
scale to zero    no                usually no           yes
idle cost        full              full                 zero
max duration     unbounded         unbounded            15 min typical
state            local disk ok     ephemeral            none
ops burden       patching, AMIs    image builds         near zero
best for         steady, stateful  most services        spiky, event-driven
worst for        spiky traffic     very short jobs      long CPU-bound work

The row that decides most real answers is scale to zero. A development environment used eight hours a day costs a third as much on serverless before any other consideration.

Kubernetes: when it is the wrong answer

aws eks describe-cluster --name demo --query 'cluster.{status:status,version:version}'
{
    "status": "ACTIVE",
    "version": "1.31"
}
CONTROL_PLANE = 0.10 * 730        # per cluster, per month
NODES = 3 * 0.0832 * 730          # three small nodes
print(f"control plane   ${CONTROL_PLANE:>8,.2f}")
print(f"nodes (3)       ${NODES:>8,.2f}")
print(f"before any workload runs: ${CONTROL_PLANE + NODES:>8,.2f}/month")
control plane   $   73.00
nodes (3)       $  182.21
before any workload runs: $  255.21/month

$255 a month and an ongoing operational commitment — version upgrades every few months, CNI and ingress configuration, RBAC, and someone who can debug a CrashLoopBackOff at 3am.

That is a good trade for thirty services and a platform team. For three services and four engineers it is a bad one, and saying so is a stronger answer than reaching for it:

“I’d want to know team size and how many services before proposing Kubernetes. For three services I’d use a managed container service — same containers, no control plane to operate. The point at which Kubernetes pays off is usually when you have enough services that the scheduling and service-discovery problems are real, or when you need portability across clouds.”

Spot instances: the arithmetic and the catch

aws ec2 describe-spot-price-history --instance-types m7g.large \
  --product-descriptions "Linux/UNIX" --max-items 3 \
  --query 'SpotPriceHistory[].{az:AvailabilityZone,price:SpotPrice}' --output table
------------------------------------
|   DescribeSpotPriceHistory       |
+--------------+-------------------+
|      az      |      price        |
+--------------+-------------------+
|  us-east-1a  |  0.0281           |
|  us-east-1b  |  0.0294           |
|  us-east-1d  |  0.0312           |
+--------------+-------------------+

$0.0281 against $0.0832 on demand — 66% off. The catch is a two-minute termination notice at any time.

GOOD FOR SPOT                     BAD FOR SPOT
batch jobs, ETL                   the last replica of anything
CI runners                        stateful databases
stateless web tier behind an LB   long jobs with no checkpointing
ML training with checkpoints      anything with a strict deadline

The design pattern worth naming: a mixed autoscaling group — a baseline of on-demand or reserved capacity for the floor, spot for the burst above it. It gets most of the discount without risking the floor.

Recognising it

QUESTION SIGNAL                                 LIKELY ANSWER
"traffic is very spiky / event-driven"          serverless
"a few requests an hour"                        serverless — scale to zero
"steady load, 24/7"                             reserved VMs or containers
"needs GPUs" / "runs for hours"                 VMs; serverless has a duration limit
"we have 40 microservices"                      Kubernetes starts to pay off
"we have 3 services and 4 engineers"            managed containers, not Kubernetes
"cost is the primary constraint"                reserved + spot mix, measure utilisation
"strict p99 latency budget"                     ask about cold starts before choosing
"lift and shift an existing app"                VMs first, refactor later

Practice

1. Compute the serverless/VM break-even at 150 ms and 0.5 GB.
~42M requests/month against one on-demand VM — but the VM cannot serve that load.
Against a correctly-sized fleet, serverless stays cheaper at every volume tested.

The naive comparison against a single VM is the common error. Size the VM fleet for the same load before comparing.

2. Vary duration from 50 ms to 2 s and watch the crossover.
0.05s/0.25G: 149M requests      2.0s/2.0G: 0.9M requests      ~164x

Duration and memory decide this, not the platform. A reserved instance moves every crossover left by about 60%.

3. Read Init Duration in a Lambda report.
Init Duration: 118.71 ms   — present on cold starts only

100-300 ms for a small interpreted function, seconds for a JVM. It only matters on a synchronous user-facing path.

4. Price an empty Kubernetes cluster.
control plane $73 + 3 nodes $182 = $255/month before any workload

Plus upgrades, CNI, RBAC, and someone on call. Good for thirty services, bad for three.

Next: storage and data transfer — the classes, the retrieval fees, and the egress line that dominates the bill.

Frequently Asked Questions

When does serverless become more expensive than a VM?
When utilisation is high enough that you would keep the VM busy anyway. Serverless charges per invocation and per gigabyte-second of execution; a VM charges by the hour whether busy or not. The crossover is usually somewhere between 15% and 40% sustained utilisation, and the arithmetic is worth doing out loud rather than asserting.
What actually causes a cold start?
The platform has to allocate a sandbox, download and unpack your code, start the runtime, and run your initialisation before the first request is handled. Typical cold starts run from about 100 ms for a small interpreted function to several seconds for a JVM with a large dependency tree; warm invocations skip all of it.
Is Kubernetes the right answer in an interview?
Only if the requirements justify it. A managed control plane costs around $70 a month before a single workload runs, plus the operational burden of upgrades, networking, and RBAC. For three services and one team, a managed container service is usually the better answer — and saying so demonstrates judgement rather than reflex.
How do I choose without knowing the traffic pattern?
Ask. The single most decision-relevant fact is whether load is steady or spiky. Steady load favours reserved VMs or containers; spiky and low-average load favours serverless; unpredictable bursts favour containers with fast autoscaling. Choosing before knowing is the actual mistake.