The False Dichotomy
The question "serverless or containers?" starts from a flawed premise. In 2025, modern cloud architectures almost always combine both paradigms, selecting each based on the characteristics of the individual workload. The goal is not to pick a side, but to recognise which tool best serves each component. This guide provides the decision framework we use with our clients.
Fundamental Characteristics of Each Paradigm
Serverless (Lambda, Cloud Run, Azure Functions)
Serverless executes your code in response to events in ephemeral environments. You pay only for actual invocations — zero cost at idle. But this model comes with hard constraints:
- Execution time limit: 15 minutes maximum on AWS Lambda, 60 minutes on Cloud Run
- Cold starts: 50 ms to 3 seconds depending on the runtime and package size, on every first invocation after a period of inactivity
- Per-invocation billing: very cost-effective for sporadic traffic, potentially expensive for sustained high-frequency traffic
- Automatic scaling: from 0 to thousands of concurrent executions without configuration — both the strength and the risk (an infinite loop can be costly)
- Ephemeral storage: /tmp limited to 512 MB (10 GB with Lambda SnapStart), no state between invocations
Containers (ECS Fargate, GKE, AKS, Cloud Run Jobs)
Containers run in persistent environments. The process starts once and stays alive to serve multiple requests, offering radically different characteristics:
- No duration limit: long-running processes (ML inference, streaming, WebSockets) are native
- Predictable latency: no cold starts, persistent database connections (connection pooling)
- Full control: custom runtime, system libraries, environment variables, fine-grained network configuration
- Hourly billing: fixed cost even without traffic — cost-effective only with high utilisation rates
- Configured scaling: HPA or ECS Service Auto Scaling — scaling configuration is your responsibility
Decision Framework — 8 Criteria
| Criterion | Serverless | Containers |
|---|---|---|
| Traffic pattern | Sporadic / unpredictable spikes | Continuous / predictable |
| Latency requirements | P99 >200 ms acceptable | P99 <100 ms required |
| Execution duration | <15 min | Unlimited |
| State & connections | Stateless only | Stateful, persistent connections |
| Cost model | Pay-per-use (optimal <5M req/month) | Reserved capacity (optimal >50M req/month) |
| Team expertise | Function-oriented developers | Kubernetes/Docker skills |
| Vendor lock-in | High (proprietary triggers) | Low (Docker standard) |
| Local development | Difficult (SAM, emulators) | Simple (docker-compose) |
Recommended Hybrid Architecture
The most effective pattern we deploy uses Lambda for everything asynchronous and event-driven, and Fargate for latency-critical synchronous APIs. Here is a concrete e-commerce example:
# Hybrid e-commerce architecture
# Synchronous layer (Fargate) — latency-critical
Main REST API → ECS Fargate (3 tasks minimum, ALB)
Authentication service → ECS Fargate (persistent DB connections)
WebSocket chat → ECS Fargate (long-lived connections)
# Asynchronous layer (Lambda) — event-driven
S3 PUT product image → Lambda resize + WebP conversion
SQS order placed → Lambda validation + email send
EventBridge cron 02:00 → Lambda nightly report generation
SNS payment notification → Lambda inventory update
DynamoDB Streams → Lambda sync to ElasticSearch
# Batch layer (Fargate Spot) — cost-optimised
Daily analytics → ECS Fargate Spot (45-min task)
Customer CSV export → ECS Fargate Spot (task > 15 min)
ML batch inference → ECS Fargate GPU (heavy task)
Cold Start Mitigation Strategies
Cold starts remain the main barrier to serverless adoption for latency-sensitive APIs. The available solutions in 2025:
# Option 1: Provisioned Concurrency
# Keeps N instances warm — cost: ~$0.015/GB-h
resource "aws_lambda_provisioned_concurrency_config" "api" {
function_name = aws_lambda_function.api.function_name
qualifier = aws_lambda_alias.live.name
provisioned_concurrent_executions = 5
}
# Option 2: Lambda SnapStart (Java only)
# Snapshots the post-init state — cold start reduced to ~100ms
resource "aws_lambda_function" "java_api" {
runtime = "java21"
snap_start {
apply_on = "PublishedVersions"
}
}
# Option 3: Optimised container image (Node.js)
# Smaller image = faster cold start
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
# Image < 50 MB = cold start < 200ms
Cost Comparison: 10 Million Requests per Month
Scenario 1 — sporadic traffic (peaks 10x baseline):
# Lambda (512 MB, 300 ms average duration)
Compute : 10M × 0.3s × 0.0000000083 $/GB-s × 0.5 GB = $0.01
Requests : 10M × 0.0000002 $ = $2.00
Lambda total: ~$2/month
# Fargate (0.25 vCPU, 0.5 GB, 1 task running 24/7 to absorb spikes)
vCPU : 0.25 × $0.04048/h × 730h = $7.39
Memory : 0.5 × $0.004445/h × 730h = $1.62
Fargate total: ~$12/month (1 task)
→ Lambda is 6x cheaper for sporadic traffic
Scenario 2 — sustained traffic (100 req/s continuously):
# Lambda (512 MB, 200 ms, 259M req/month)
Compute : 259M × 0.2s × 0.0000000083 × 0.5 = $0.21
Requests : 259M × 0.0000002 = $51.80
Lambda total: ~$52/month
# Fargate (2 tasks at 1 vCPU to handle 100 req/s)
vCPU : 2 × 1 × $0.04048 × 730 = $59.10
Memory : 2 × 2 × $0.004445 × 730 = $12.98
Fargate total: ~$72/month (or ~$50 with Savings Plans)
→ Comparable cost, but Fargate delivers lower latency and persistent connections
Advanced Serverless Patterns
Serverless shines in asynchronous architectural patterns that containers handle less elegantly:
- Fan-out: one SNS event simultaneously triggers N Lambda functions in parallel — distributed processing without orchestration
- Saga (Step Functions): orchestration of distributed transactions with automatic compensation on failure
- Event Sourcing with EventBridge: business events published to EventBridge trigger Lambda consumers without direct service coupling
- CQRS: DynamoDB Streams → Lambda to project writes to optimised read models (ElasticSearch, Redis)
Advanced Container Patterns
- Sidecar: proxy container (Envoy, AWS App Mesh) attached to the application container to handle TLS, retries, and circuit breaking
- Service mesh: Istio or Linkerd for inter-service observability and security without modifying application code
- Blue/Green with Fargate: ECS + CodeDeploy for zero-downtime deployments with instant rollback
- Spot Instances: ECS Fargate Spot for interruption-tolerant workloads — 70% cost saving on batch tasks
Conclusion: Choose by Traffic Shape, Not by Hype
In 2025, the answer to "serverless or containers?" is almost always "both, depending on the workload." The golden rule: analyse the traffic shape of each component, not its technical complexity. An S3 event that triggers processing three times per hour → Lambda without hesitation. An API receiving 200 requests per second continuously with persistent PostgreSQL connections → Fargate without hesitation. The mistake is forcing a single paradigm onto the entire architecture for the sake of simplicity. A well-designed architecture embraces this heterogeneity as a strength, assigning the right tool to each component and letting workload characteristics — not trends — drive every decision.
