Serverless Architecture: Patterns & Anti-Patterns
Design serverless systems that scale and don't surprise you with costs. Covers event-driven patterns, cold starts, state management, orchestration, and when serverless is the wrong choice.
Serverless doesn’t mean “no servers” — it means someone else manages the servers. It shines for event-driven, spiky workloads where you pay only for what you use. It fails spectacularly for long-running, steady-state workloads where you end up paying more than containers or VMs. Know the patterns, know the traps, and know when to walk away.
The promise of serverless is compelling: no infrastructure management, automatic scaling from zero to millions of requests, and a pay-per-invocation model that starts at effectively free. The reality is more nuanced — cold starts, execution limits, debugging complexity, and vendor lock-in are real costs that don’t appear on the bill.
Serverless Compute Options
| Service | AWS | Azure | GCP | Max Duration |
|---|---|---|---|---|
| Functions | Lambda | Azure Functions | Cloud Functions | 15 min (Lambda) |
| Containers | Fargate | Container Apps | Cloud Run | Unlimited |
| Orchestration | Step Functions | Durable Functions | Workflows | 1 year (Step Fn) |
| API Gateway | API Gateway / ALB | API Management | API Gateway | N/A |
| Event bus | EventBridge | Event Grid | Eventarc | N/A |
| Queues | SQS | Queue Storage | Pub/Sub | N/A |
Patterns That Work
1. API Backend (Most Common)
The classic serverless pattern: HTTP request → function → database → response.
# AWS Lambda + API Gateway
import json
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('orders')
def handler(event, context):
method = event['httpMethod']
if method == 'GET':
order_id = event['pathParameters']['id']
result = table.get_item(Key={'order_id': order_id})
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps(result.get('Item', {}))
}
elif method == 'POST':
body = json.loads(event['body'])
body['created_at'] = datetime.utcnow().isoformat()
table.put_item(Item=body)
return {
'statusCode': 201,
'body': json.dumps({'message': 'Created', 'id': body['order_id']})
}
2. Event Processing Pipeline (Fan-Out)
Decouple processing stages with queues. Each stage scales independently.
S3 Upload → Lambda (Validate) → SQS → Lambda (Process) → DynamoDB
↓ (on failure)
Dead Letter Queue → Lambda (Alert + Log)
EventBridge Rule → Lambda (Enrich) → Kinesis → Lambda (Aggregate) → S3
3. Scheduled Jobs (Cron Replacement)
Replace cron servers with scheduled Lambda functions:
# CloudWatch Events → Lambda (runs every hour)
def handler(event, context):
stale = find_stale_sessions(hours=24)
for session in stale:
cleanup_session(session['id'])
expired_trials = find_expired_trials()
for trial in expired_trials:
downgrade_to_free(trial['user_id'])
return {'sessions_cleaned': len(stale), 'trials_expired': len(expired_trials)}
4. File Processing (Image/Video/Document)
# S3 trigger → Lambda processes uploaded file
def handler(event, context):
bucket = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
# Download, process, upload result
image = download_from_s3(bucket, key)
thumbnail = create_thumbnail(image, size=(300, 300))
upload_to_s3(bucket, f"thumbnails/{key}", thumbnail)
# Update metadata in database
update_image_status(key, status='processed')
Anti-Patterns
1. Monolith-in-a-Lambda
# ❌ Don't cram your entire app into one function
def handler(event, context):
if event['path'] == '/users': return handle_users(event)
elif event['path'] == '/orders': return handle_orders(event)
elif event['path'] == '/payments': return handle_payments(event)
elif event['path'] == '/reports': return handle_reports(event)
# 50 more routes...
# Problems:
# - Cold start loads ALL dependencies even for one route
# - Deployment changes affect all routes
# - Cannot scale routes independently
# - Debugging is harder (which route caused the error?)
# ✅ One function per route (or per domain)
# /users → users_handler.py
# /orders → orders_handler.py
# /payments → payments_handler.py
2. Lambda Calling Lambda (Synchronous Chain)
# ❌ Synchronous function chains — brittle, expensive, compounding latency
Lambda A → Lambda B → Lambda C → Lambda D
(A pays for time waiting for B, B pays for time waiting for C...)
Total latency = sum of all functions + cold starts
Total cost = 4x what you'd expect
# ✅ Use Step Functions for orchestration
Step Functions: A → B → C → D
(Built-in retries, error handling, parallel branches, wait states)
# ✅ Or use async events for decoupled processing
Lambda A → SQS → Lambda B → SQS → Lambda C
(Each function runs independently, retries independently)
3. Ignoring Cold Starts
Cold starts happen when a new execution environment is created — the first request is slow.
| Runtime | Cold Start (avg) | Warm Invocation | Provisioned |
|---|---|---|---|
| Python 3.12 | 200-500ms | 1-5ms | ~0ms (always warm) |
| Node.js 20 | 100-300ms | 1-5ms | ~0ms |
| Java 21 | 1-5 seconds | 1-10ms | ~0ms |
| .NET 8 | 500ms-2s | 1-10ms | ~0ms |
| Rust / Go | 50-100ms | <1ms | ~0ms |
Mitigation Strategies:
| Strategy | Cost | Effectiveness | Use When |
|---|---|---|---|
| Provisioned concurrency | $$$ | Eliminates cold starts | User-facing, latency-sensitive |
| Keep-warm pings | $ | Reduces cold starts | Low-traffic APIs |
| Smaller deployment packages | Free | Faster init | Always — remove unused dependencies |
| SnapStart (Java) | Free | 90% reduction | Java on Lambda |
| Choose lighter runtimes | Free | Faster by default | New projects (Python, Node, Rust) |
# Provisioned concurrency configuration (SAM template)
Resources:
OrderFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.12
MemorySize: 512
AutoPublishAlias: live
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 10
Cost Analysis
Understanding the cost crossover point is critical for choosing between serverless and containers.
When Serverless Is Cheaper
Low/spiky traffic: < 1M requests/month, 200ms avg execution
- Lambda: ~$3-5/month (compute + requests)
- EC2 t3.small (24/7): ~$15/month
- Fargate (0.25 vCPU): ~$9/month
Winner: Lambda ✅ (especially with scale-to-zero)
When Serverless Is Expensive
Steady high traffic: 100M requests/month, 200ms avg, 512MB memory
- Lambda: ~$400/month (compute) + $60 (requests) = $460/month
- Fargate (2 vCPU, 4GB): ~$120/month
- EC2 reserved (m5.large): ~$50/month
Winner: EC2 or Fargate ✅ (3-9x cheaper at steady load)
Rule of Thumb: If your function runs > 50% of the time at steady load, containers or VMs are cheaper. If traffic is bursty (0-1000 RPS within minutes), serverless wins on both cost and operations.
Hidden Costs
| Hidden Cost | Impact | Mitigation |
|---|---|---|
| Data transfer between services | $0.01-0.09/GB | Minimize cross-region calls |
| API Gateway per-request fee | $3.50/million requests | Use ALB ($0.40/million) for high volume |
| CloudWatch Logs ingestion | $0.50/GB | Filter logs, use sampling |
| Provisioned concurrency | $$ always-on cost | Only for latency-critical functions |
When NOT to Go Serverless
| Workload | Why Not | Better Option |
|---|---|---|
| WebSocket / long-lived connections | Lambda max 15 min, no persistent connections | Fargate, EC2, Cloud Run |
| Steady-state high throughput | Cost inefficient at sustained load | Containers (Fargate, ECS, K8s) |
| GPU / ML inference | No GPU support in Lambda | SageMaker, GPU instances, Cloud Run GPU |
| Legacy apps requiring local filesystem | Stateless functions, /tmp limited to 512MB-10GB | Containers, VMs |
| Sub-10ms latency required | Cold starts unpredictable | Containers with health checks |
| Complex multi-step transactions | Difficult to manage distributed state | Containers with SAGA pattern |
Serverless Observability
Serverless makes debugging harder because there are no servers to SSH into. Invest in observability from day one.
| Layer | Tool | Why |
|---|---|---|
| Distributed traces | AWS X-Ray, Datadog, Honeycomb | See request flow across functions |
| Structured logging | CloudWatch Logs Insights, Datadog | Query logs without grep |
| Custom metrics | CloudWatch EMF, Datadog | Business-specific measurements |
| Error tracking | Sentry, Lumigo | Stack traces with context |
Serverless Cost Traps
| Trap | How It Happens | Prevention |
|---|---|---|
| Recursive invocations | Lambda triggers itself via S3 or SNS loop | Dead letter queues, invocation limits |
| Cold start cascading | Downstream Lambdas cold-start in sequence | Provisioned concurrency for critical paths |
| Excessive logging | CloudWatch costs exceed compute costs | Structured logging, sampling, log levels |
| Over-provisioned memory | Setting 1GB when 256MB suffices | Memory profiling with AWS Lambda Power Tuning |
| Idle DynamoDB capacity | Provisioned throughput for peak, idle 23 hours per day | Switch to on-demand for variable workloads |
| Unnecessary API Gateway | Using API Gateway when ALB + Lambda suffices | Use ALB for internal APIs (10x cheaper) |
When NOT to Go Serverless
- Long-running processes (over 15 minutes) — Use ECS or Fargate or Step Functions instead
- Stateful applications — WebSocket servers, game servers, real-time collaboration
- GPU workloads — ML inference, video processing (use ECS with GPU instances)
- High-throughput streaming — Sustained 100K+ events per sec (consider Kinesis + EC2)
- Predictable, constant load — If traffic is steady 24/7, reserved EC2 is cheaper
Checklist
- Workload pattern analyzed (spiky vs steady) — serverless appropriate?
- Cold start impact assessed for user-facing functions
- Cost modeling done at projected scale with crossover analysis
- Event-driven architecture designed (not synchronous chains)
- Dead letter queues configured for every async trigger
- Observability set up (distributed tracing + structured logging)
- Function deployment packages minimized (remove unused deps)
- Timeout and memory configured per function (not defaults)
- API Gateway throttling and usage plans configured
- Provisioned concurrency set for latency-critical paths
- Monitoring alerts on error rate, duration, and throttling
:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For cloud architecture consulting, visit garnetgrid.com. :::