Serverless vs Containers on AWS: Choosing the Right Architecture
"Should this be Lambda or a container?" comes up on nearly every AWS architecture decision, and the honest answer is "it depends on the shape of the workload," not on which technology is newer or more fashionable. Here's how to actually make that call.
How the Two Models Differ
Serverless (Lambda) runs your code on demand, in response to an event, and you're billed per invocation and execution time. There's no server to manage, no idle capacity to pay for, and scaling to zero (and back up) is automatic.
Containers (ECS/EKS/Fargate) run your application continuously (or on a schedule you control) inside a managed cluster. You have more control over the runtime environment, longer-running processes are natural, and you pay for allocated capacity rather than per-invocation.
Where Lambda Wins
- Spiky, unpredictable traffic. If load is bursty or has long idle periods, paying only for actual invocations beats paying for always-on capacity.
- Event-driven workloads. Processing S3 uploads, responding to SQS messages, running scheduled jobs — Lambda's event integration with the rest of AWS is native and low-friction.
- Short-lived, stateless tasks. Anything that completes in seconds and doesn't need to hold state between requests fits Lambda's execution model naturally.
- Minimal operational overhead. No patching, no cluster management, no capacity planning for idle periods.
The trade-offs
- Cold starts. A function that hasn't run recently pays a startup penalty, which matters for latency-sensitive, user-facing requests. Provisioned concurrency mitigates this at extra cost.
- Execution time and payload limits. Long-running or resource-heavy workloads (video processing, large batch jobs) push against Lambda's constraints.
- Cost at sustained high volume. Per-invocation pricing that looks cheap at low volume can exceed the cost of reserved container capacity once traffic is consistently high.
Where Containers Win
- Long-running or stateful processes. WebSocket servers, background workers that maintain in-memory state, anything that benefits from a warm, persistent runtime.
- Predictable, sustained load. If traffic is steady rather than spiky, reserved or provisioned container capacity is typically cheaper than equivalent Lambda invocations.
- Complex runtime dependencies. Large binaries, GPU workloads, or environments with dependencies that don't fit Lambda's deployment package limits are a natural fit for containers.
- Fine-grained control. Custom networking, specific OS-level configuration, or multi-process architectures are easier to express in a container than around Lambda's constraints.
The trade-offs
- Operational overhead. Even with Fargate removing server management, you're still managing task definitions, scaling policies, and cluster-level configuration.
- Idle cost. Unless scaling policies are tuned carefully, containers sitting at minimum capacity cost money even with no traffic.
A Decision Framework
| Workload characteristic | Lean toward |
|---|---|
| Traffic is bursty or has long idle periods | Lambda |
| Traffic is steady and predictable | Containers |
| Task completes in seconds, is stateless | Lambda |
| Process is long-running or holds state | Containers |
| Minimal ops overhead is the priority | Lambda |
| Fine-grained runtime control is needed | Containers |
| Sub-100ms latency on every request is critical | Containers (with warm capacity) or Lambda + provisioned concurrency |
They're Not Mutually Exclusive
Most production AWS architectures we build use both: Lambda for event handling, scheduled jobs, and glue logic between services; containers for the core application that serves sustained traffic. Trying to force an entire system into one model usually means fighting the platform somewhere.
# Example: Lambda handling an async event, container serving the API
Resources:
ApiService:
Type: AWS::ECS::Service
Properties:
LaunchType: FARGATE
DesiredCount: 2
ImageProcessor:
Type: AWS::Lambda::Function
Properties:
Runtime: nodejs20.x
Handler: index.handler
Events:
S3Upload:
Type: S3
Properties:
Bucket: !Ref UploadsBucket
Events: s3:ObjectCreated:*
Conclusion
The question isn't "which is better" — it's "what does this specific workload's traffic and runtime profile actually look like." Start from the shape of the workload, not a platform preference, and most systems will end up using both models for the parts they're each best suited to.