What each abstraction takes away — cold starts, execution ceilings, no work after the response returns, ephemeral disk, and why an edge runtime cannot hold a WebSocket or a TCP pool.
Module F-10 — Compute Models: VMs, Containers, Serverless, and the Edge
What this module covers: Each step up the compute abstraction ladder hands you something and quietly takes something away, and the things taken away are what cause the outages. This module covers VMs, containers, serverless containers, functions, and edge runtimes — with a specific inventory of what each one removes: cold starts, execution-time and memory ceilings, the rule that no work happens after the response returns, ephemeral local disk, and the inability of an edge runtime to hold a WebSocket or a database connection pool. It ends with the constraint that decides more architectures than any other: where the code runs relative to where the data lives.
The Ladder, and the Trade at Each Rung
| Unit | Start-up | You manage | Scales to zero | Holds state between requests | |
|---|---|---|---|---|---|
| Bare metal | Machine | Minutes | Everything | No | Yes |
| VM | Guest OS | 30s–2min | OS, runtime, app | No | Yes |
| Container | Process + namespaces | 1–10s | Image, app | Rarely | Within the container's life |
| Serverless container | Container, managed | 1–10s cold | Image, app | Yes | Only while an instance is warm |
| Function (FaaS) | Handler invocation | 50ms–2s cold | App code | Yes | Unreliably |
| Edge runtime | V8 isolate | ~0–5ms | App code | Yes | No |
Read the last two columns together. As start-up time and operational burden go down, the ability to hold anything goes down with them — and "anything" includes connections, caches, sockets, timers, and in-flight background work. That's not incidental; it's the mechanism. A platform can only start you in five milliseconds and scale you to zero if it's free to discard you at any moment.
Analogy: the ladder runs from owning a house, to renting a flat, to a hotel room, to a hot-desk, to a phone booth. Each step is cheaper, faster to get into, and less of your responsibility. Each step also gives you less room to leave anything lying around — and by the phone booth, you can't even keep a phone line open between calls.
Why this matters in production: every one of these platforms runs a "hello world" identically well. The differences only appear when your code wants to keep a database connection, do work after responding, write a temp file, or hold a socket open. Those are precisely the things a real application does, and discovering the constraint after committing to the platform is the expensive path.
VMs and Containers: The Familiar End
Virtual machines give you a full guest OS with its own kernel. Strong isolation, any workload, complete control — and you own patching, configuration drift, and multi-minute boot times. Autoscaling on a two-minute boot means you're always reacting late.
Containers share the host kernel and isolate with namespaces and cgroups. Start-up drops to seconds, images are immutable and reproducible, and density goes up substantially. Two properties matter for design:
- The filesystem is ephemeral. Anything written inside a container disappears on restart unless it's on a mounted volume. This is the same "local files are state" problem as Module F-9, now enforced by the platform.
- Resource limits are enforced by cgroups, and runtimes often don't notice. A JVM or Node process that reads the host's memory rather than its cgroup limit will happily grow past the container's ceiling and be OOM-killed — a restart loop with no application error to explain it.
Containers keep the crucial property the higher rungs give up: a container is a long-running process. It can hold a connection pool, an in-memory cache, a WebSocket, a background timer. Everything below this line loses some of that.
Functions: Four Constraints That Reshape Designs
FaaS (AWS Lambda, Vercel Functions, Cloud Functions) runs a handler per request and charges per invocation-millisecond. The operational win is real — no servers, scale to zero, scale to thousands. Four constraints come with it.
1. Cold starts
When no warm instance exists, the platform provisions one: fetch the code, initialise the runtime, run module-level code, then invoke the handler. Roughly:
| Runtime / situation | Typical cold start |
|---|---|
| Node.js / Python, small bundle | 100–400 ms |
| Node.js with a large dependency tree | 500 ms – 2 s |
| JVM / .NET | 1–5 s |
| Inside a VPC (historically) | +seconds |
| Edge / V8 isolate | ~0–5 ms |
Cold starts are a tail-latency phenomenon by construction (Module F-4). Your p50 can be excellent while p99 includes a full initialisation. And the shape is spiky: a traffic surge triggers many simultaneous cold starts precisely when latency matters most.
What actually helps: smaller bundles and fewer dependencies (the single biggest lever), moving expensive setup out of the hot path, provisioned concurrency where the platform offers it — with the cost named, since provisioned concurrency is paying for idle capacity, which is most of what serverless promised to eliminate.
2. No work after the response returns
This is the constraint that breaks the most existing code. In a long-running server, this is idiomatic:
// Perfectly fine on a container. Broken on FaaS.
app.post('/orders', async (req, res) => {
const order = await createOrder(req.body);
res.json(order); // respond immediately
void sendConfirmationEmail(order); // fire and forget
void trackAnalytics('order.created', order);
void warmRecommendationCache(order.customerId);
});
On FaaS the execution environment may be frozen or destroyed the moment the response is returned. Those three background tasks might complete, might partially complete, or might never start — and it varies invocation to invocation, so it "works in staging" and drops a fraction of emails in production. Silent, partial, non-deterministic data loss is the worst failure category to debug.
The fix is architectural, not a workaround: anything that must happen goes onto a durable queue before you respond.
app.post('/orders', async (req, res) => {
const order = await createOrder(req.body);
// Enqueue durably *before* responding. The queue is the durability boundary.
await queue.sendBatch([
{ type: 'email.order_confirmation', orderId: order.id },
{ type: 'analytics.order_created', orderId: order.id },
]);
res.json(order);
});Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion