Virtual Machines vs Containers vs Serverless
Choosing a cloud compute model determines far more than where application code executes. It affects deployment speed, isolation, scaling behavior, startup latency, infrastructure control, operational workload, and cost.
Virtual machines provide strong isolation and maximum operating-system control. Containers package applications into portable runtime units while sharing the host kernel. Serverless platforms move most infrastructure management behind a managed execution environment and scale individual functions or services in response to demand.
None is universally better. Production architecture should select the model based on workload characteristics rather than infrastructure trends. Long-running stateful software, high-throughput APIs, asynchronous workers, scheduled jobs, and unpredictable event-driven workloads often require different compute strategies.
Table of Contents
- Understanding Cloud Compute Models
- Virtual Machines
- Containers
- Serverless
- Production Comparison
- Production Design Example
- Common Mistakes
- Production Checklist
- Conclusion
Understanding Cloud Compute Models
The fundamental difference between virtual machines, containers, and serverless is where the infrastructure-management boundary sits.
With virtual machines, application teams typically manage the guest operating system, runtime, application processes, patching, and much of the capacity planning. Containers remove the guest operating system from the deployment unit but still require a runtime and usually an orchestration layer. Serverless moves the execution infrastructure almost entirely behind the provider interface.
Virtual Machine
+----------------------------+
| Application |
| Runtime / Libraries |
| Guest Operating System |
+----------------------------+
| Hypervisor |
| Physical Infrastructure |
+----------------------------+
Container
+----------------------------+
| Application + Dependencies |
+----------------------------+
| Container Runtime |
| Host Operating System |
+----------------------------+
| Physical / Virtual Host |
+----------------------------+
Serverless
+----------------------------+
| Application Code |
+----------------------------+
| Managed Runtime |
| Managed Execution Platform |
| Infrastructure |
+----------------------------+
This abstraction changes operational responsibility, but it does not eliminate infrastructure concerns. A serverless application can still exhaust database connections, exceed service quotas, experience network latency, create retry storms, or fail because a downstream dependency is unavailable.
The compute layer is only one part of the larger architecture. More about how compute fits with traffic management, storage, databases, queues, and observability can be found here: Cloud Architecture Explained: Building Modern Applications.
Virtual Machines
A virtual machine provides an isolated operating-system environment running on virtualized hardware. Applications can control the operating system, installed packages, runtime versions, filesystem layout, networking configuration, background processes, and kernel-level settings available inside the guest.
This flexibility makes virtual machines suitable for workloads that cannot easily fit into more restrictive execution environments. The trade-off is that the application team owns significantly more infrastructure lifecycle management.
A typical VM deployment places multiple instances behind a load balancer:
Load Balancer
|
+------------+------------+
| | |
v v v
VM 1 VM 2 VM 3
| | |
+------------+------------+
|
v
Database
Instances should still be treated as replaceable whenever possible. Configuration should be generated from infrastructure definitions or machine images rather than manually applied after provisioning.
Advantages
- Maximum control: operating-system packages, processes, networking, runtimes, and system configuration can be customized.
- Strong workload isolation: each VM has its own guest operating system and kernel boundary.
- Broad compatibility: legacy applications and specialized software can often run without major architectural changes.
- Predictable long-running capacity: dedicated VM capacity works well for stable workloads with continuous utilization.
- Specialized hardware support: workloads can use GPU, high-memory, high-I/O, or other specialized machine types.
Disadvantages
- Slower provisioning: starting a VM usually takes longer than starting a container or warm serverless execution environment.
- Lower resource density: each VM includes an operating system and reserved resources.
- More patching: operating-system security updates and lifecycle management remain an operational responsibility.
- Coarser scaling: adding an entire machine is less granular than adding containers or individual function executions.
- Configuration drift: long-lived machines can gradually differ when configuration changes are applied manually.
When to Use Virtual Machines
Virtual machines are appropriate when the workload requires operating-system control, specialized networking, custom system software, or software that assumes a persistent machine environment.
Typical production use cases include:
- legacy applications that cannot easily be containerized;
- commercial software with OS-specific installation requirements;
- specialized databases or infrastructure software;
- GPU or high-performance computing workloads;
- applications requiring custom kernel or networking configuration;
- stable long-running workloads where reserved capacity is cost-effective.
Virtual machines should generally not be selected merely because they are familiar. For stateless APIs with frequent deployments and many independently scalable components, containers often provide a more efficient operational model.
Containers
Containers package application code together with runtime dependencies while sharing the host operating-system kernel. This creates a smaller deployment unit than a VM and allows many isolated application processes to run efficiently on the same underlying compute capacity.
The important architectural advantage is environment consistency. The same application image can move through development, testing, staging, and production without rebuilding the runtime environment on every machine.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
# The container owns the application process,
# while durable state remains outside the container.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
Production container platforms normally schedule containers across a fleet of machines:
Load Balancer
|
+----------+----------+
| |
v v
Container Container
| |
+-----+-----+ +-----+-----+
| Node A | | Node B |
+-----------+ +-----------+
\ /
+---------+---------+
|
v
Database
The scheduler handles placement, restart, rolling deployment, and capacity allocation. Containers themselves remain disposable.
Advantages
- Fast startup: containers generally start much faster than complete virtual machines.
- High resource density: many containers can share the same host kernel and underlying machine.
- Portable deployment artifact: application runtime and dependencies are packaged together.
- Efficient horizontal scaling: individual application replicas can be added or removed quickly.
- Strong CI/CD integration: immutable images work naturally with automated deployment pipelines.
- Good service isolation: APIs, workers, scheduled jobs, and other workloads can scale independently.
Disadvantages
- Orchestration complexity: large deployments require scheduling, networking, service discovery, health checks, and deployment automation.
- Cluster capacity still exists: containers require underlying compute unless using a fully managed container runtime.
- Shared kernel: isolation differs from a full VM boundary.
- State requires careful design: local container filesystems should normally be considered ephemeral.
- Resource configuration matters: incorrect CPU and memory requests can waste capacity or cause workload instability.
When to Use Containers
Containers are a strong default for long-running stateless applications that require predictable execution environments and independent horizontal scaling.
Typical production workloads include:
- REST and GraphQL APIs;
- microservices;
- background workers;
- web applications;
- stream processors;
- scheduled jobs;
- internal services with custom runtime dependencies.
Consider an API with a CPU request of 500 millicores and a memory request of 512 MiB. Multiple replicas can be distributed across larger worker nodes, allowing infrastructure capacity to be shared efficiently.
apiVersion: apps/v1
kind: Deployment
metadata:
name: shipment-api
spec:
replicas: 4
selector:
matchLabels:
app: shipment-api
template:
metadata:
labels:
app: shipment-api
spec:
containers:
- name: api
image: registry.example.com/shipment-api:2026.08.27
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
memory: "768Mi"
readinessProbe:
httpGet:
path: /health/ready
port: 8080
Resource requests help the scheduler place workloads correctly. Memory limits protect neighboring workloads, while CPU configuration should be selected carefully because aggressive CPU limits can introduce throttling during traffic bursts.
Serverless
Serverless compute executes application code through a managed platform without requiring application teams to provision or operate individual servers. Execution is typically triggered by HTTP requests, queue messages, object-storage events, database events, or schedules.
The primary architectural difference is that capacity is allocated around executions rather than around long-running application hosts.
Object Uploaded
|
v
Object Storage
|
v
Serverless Function
|
+------> Metadata Database
|
+------> Processed Object
For example, an image-processing function can run only when a new object appears:
from dataclasses import dataclass
@dataclass(frozen=True)
class ImageJob:
bucket: str
object_key: str
def handler(event: dict, context: object) -> dict[str, str]:
job = ImageJob(
bucket=event["bucket"],
object_key=event["object_key"],
)
image = object_storage.download(
bucket=job.bucket,
key=job.object_key,
)
thumbnail = image_processor.resize(
image=image,
width=400,
)
output_key = f"thumbnails/{job.object_key}"
object_storage.upload(
bucket=job.bucket,
key=output_key,
content=thumbnail,
)
return {"output_key": output_key}
No permanently running image-processing fleet is required. If no images arrive, no application compute needs to remain active.
Advantages
- Minimal infrastructure management: machine provisioning and operating-system maintenance are handled by the platform.
- Fine-grained scaling: execution capacity can grow rapidly with incoming events.
- Efficient for intermittent workloads: applications avoid paying continuously for mostly idle compute.
- Natural event integration: queues, schedules, storage events, and API requests can trigger execution directly.
- Small deployment units: individual functions or services can be deployed independently.
Disadvantages
- Execution constraints: runtime duration, memory, temporary storage, concurrency, and networking may be restricted.
- Cold starts: new execution environments can introduce additional latency.
- Limited runtime control: operating-system and execution-environment customization is restricted.
- Cost can grow rapidly: high-volume continuous workloads may become more expensive than provisioned compute.
- Downstream pressure: rapid function scaling can overwhelm databases or external APIs.
- Platform coupling: event models, permissions, deployment configuration, and integrations may become provider-specific.
When to Use Serverless
Serverless is particularly effective for event-driven, intermittent, bursty, and relatively short-lived workloads.
Typical examples include:
- file and image processing;
- webhooks;
- scheduled maintenance jobs;
- queue consumers;
- low-volume APIs;
- event transformation;
- notification workflows;
- automation and infrastructure events.
A common production mistake is assuming that automatic function scaling means the entire application scales automatically. A function may grow from tens to thousands of concurrent executions while all of those executions attempt to connect to the same database.
2,000 Functions
|
v
+-------------------+
| Connection Proxy |
+---------+---------+
|
v
Database
max connections: 500
Concurrency therefore needs explicit control. Connection proxies, queues, reserved concurrency, rate limits, and backpressure can protect downstream systems from serverless fan-out.
Production Comparison
The compute models become easier to compare when evaluated through production concerns rather than implementation style.
| Property | Virtual Machines | Containers | Serverless |
|---|---|---|---|
| Infrastructure Control | High | Medium to high | Low |
| Startup Time | Slowest | Fast | Fast, but cold starts possible |
| Scaling Granularity | Machine | Container replica | Execution |
| OS Management | Application team | Host/platform layer | Provider |
| Resource Efficiency | Lower | High | High for intermittent workloads |
| Long-Running Workloads | Excellent | Excellent | Platform-dependent |
| Bursty Event Processing | Requires capacity management | Good with autoscaling | Excellent |
| Runtime Flexibility | Highest | High | Restricted |
| Operational Complexity | OS and infrastructure operations | Orchestration and platform operations | Application and integration operations |
| Typical Billing | Provisioned machine capacity | Underlying or requested resources | Requests and execution resources |
Serverless reduces infrastructure operations but can increase application-level distributed-systems concerns. Containers reduce deployment friction but introduce orchestration. Virtual machines maximize control but require greater lifecycle management.
Abstraction does not remove complexity; it moves complexity to a different layer.
Production Design Example
A real cloud platform does not need to select one compute model globally. Different workloads can use different models when their operational characteristics justify it.
Consider an e-commerce platform with interactive APIs, asynchronous media processing, and a legacy reporting engine:
Users
|
v
CDN / Gateway
|
v
Containerized APIs
/ | \
/ | \
v v v
Database Cache Queue
|
+----------------+---------------+
| |
v v
Container Workers Serverless Functions
Order Processing Image Processing
|
v
External Services
Scheduled Reporting
|
v
Virtual Machine
Legacy Analytics
|
v
Database
Containerized APIs handle continuous customer traffic. They require fast deployments, predictable runtime behavior, and horizontal scaling, making containers a natural fit.
Container workers process long-running order workflows. Queue depth can control worker scaling independently from HTTP traffic.
Serverless functions resize product images when objects are uploaded. The workload is bursty and event-driven, so permanently running workers would spend significant time idle.
A virtual machine hosts an older reporting engine that requires specific operating-system packages and local system configuration. Rewriting it solely to standardize the compute model would create migration risk without necessarily creating equivalent business value.
The architecture deliberately uses multiple compute models because standardizing everything on one runtime can be more expensive than operating a small number of justified exceptions.
Observability should remain consistent across these environments. Logs should include common request and correlation identifiers, metrics should use compatible service naming, and distributed traces should cross container and serverless boundaries where possible.
Scaling also needs to consider downstream capacity globally. If containers and serverless functions both access the same database, their combined maximum concurrency must remain compatible with database connection and transaction capacity.
Common Mistakes
Compute-model mistakes usually appear when architecture decisions optimize for deployment convenience while ignoring workload behavior and downstream constraints.
| Mistake | Production Impact | Better Approach |
|---|---|---|
| Choosing one compute model for every workload | Different workloads inherit unnecessary cost or operational constraints. | Select compute according to workload duration, traffic shape, isolation, and control requirements. |
| Treating VMs as permanent servers | Manual configuration creates drift and makes recovery slow. | Build reproducible images and replace instances rather than repairing them manually. |
| Writing persistent data inside containers | Rescheduling or deployment can destroy application state. | Externalize durable state to databases or storage services. |
| Running containers without realistic resource requests | Schedulers overcommit nodes or waste large amounts of capacity. | Measure workload consumption and define production-specific resource requests. |
| Assuming serverless has unlimited scaling | Functions hit concurrency quotas or overwhelm downstream systems. | Model end-to-end capacity and apply concurrency limits and backpressure. |
| Ignoring serverless cold starts | Latency-sensitive endpoints experience unpredictable tail latency. | Measure cold-start behavior and avoid unsuitable runtimes on critical low-latency paths. |
| Using serverless for continuously busy workloads without cost analysis | Per-execution pricing may exceed provisioned compute cost. | Compare cost at expected sustained utilization, not only low traffic. |
| Running an orchestration platform for a few simple services | Platform complexity exceeds the operational value it provides. | Use the simplest managed runtime that satisfies deployment and scaling requirements. |
| Ignoring shutdown behavior | Deployments terminate requests or partially processed jobs. | Implement graceful shutdown, connection draining, and bounded termination periods. |
| Optimizing compute without considering dependencies | Autoscaling moves saturation to databases, queues, or external APIs. | Capacity-plan the entire request and processing chain. |
Production Checklist
The compute model should be validated against operational behavior before production traffic depends on it.
- Measure workload shape: determine whether traffic is continuous, predictable, bursty, scheduled, or event-driven.
- Define startup requirements: measure how quickly new capacity must become ready during traffic spikes or failures.
- Externalize durable state: make compute instances replaceable without losing application data.
- Model maximum concurrency: verify that databases, caches, queues, and external APIs can support peak compute concurrency.
- Configure graceful shutdown: allow active requests and jobs to complete or return safely to their queues.
- Set resource boundaries: define realistic CPU, memory, concurrency, and execution limits.
- Test scaling behavior: verify scale-out and scale-in under realistic traffic instead of relying only on configuration.
- Measure tail latency: compare p95 and p99 latency, including container startup and serverless cold-start behavior.
- Calculate sustained cost: compare provisioned and consumption-based pricing using expected utilization patterns.
- Automate deployments: build reproducible VM images, container images, or serverless packages through CI/CD.
- Test compute failure: terminate instances and executions deliberately and verify automatic recovery.
- Standardize observability: propagate common metrics, logs, traces, and correlation identifiers across all compute models.
Conclusion
Virtual machines, containers, and serverless represent different infrastructure abstraction levels rather than generations where one replaces another. Virtual machines provide maximum control, containers provide an efficient portable execution model, and serverless provides fine-grained managed execution for workloads that fit its constraints.
Containers are often a strong choice for continuously running stateless APIs and workers. Serverless performs particularly well for intermittent event-driven processing. Virtual machines remain valuable when applications require operating-system control, specialized environments, or compatibility with software that does not fit newer execution models.
Production systems can combine all three. The important architectural requirement is maintaining clear state boundaries, consistent observability, controlled concurrency, and capacity planning across the entire system.
Key Takeaway: Choose the compute model according to workload behavior. More infrastructure abstraction reduces some operational responsibilities, but every model retains performance, reliability, scaling, and cost trade-offs that must be managed explicitly.
Comments (0)