Serverless on AWS isn't "just use Lambda." It's a design philosophy: let AWS manage the infrastructure, pay only for what you use, and build with managed services that scale independently. But the patterns that work in serverless are fundamentally different from traditional architectures — and the anti-patterns are expensive to learn the hard way.
This guide covers the patterns that work in production, the anti-patterns that waste money or cause outages, and the decision framework for when serverless is the right (or wrong) choice.
The Serverless Building Blocks
┌─────────────────────────────────────────────────────────────────────┐
│ AWS SERVERLESS STACK │
├─────────────────────────────────────────────────────────────────────┤
│ COMPUTE │ Lambda | Fargate (serverless containers) │
│ API │ API Gateway (REST/HTTP/WebSocket) | AppSync (GraphQL)│
│ ORCHESTRATION │ Step Functions | EventBridge Scheduler │
│ MESSAGING │ SQS | SNS | EventBridge │
│ STORAGE │ S3 | DynamoDB | Aurora Serverless │
│ STREAMING │ Kinesis | DynamoDB Streams | MSK Serverless │
│ AUTH │ Cognito | IAM | Lambda Authorizers │
│ OBSERVABILITY │ CloudWatch | X-Ray | Application Signals │
└─────────────────────────────────────────────────────────────────────┘
Key principle: In serverless, you compose applications from managed services. Lambda is the glue between them — not the application itself.
Pattern 1: Synchronous API (Request/Response)
The most common serverless pattern: HTTP API backed by Lambda.
Client → API Gateway → Lambda → DynamoDB / Aurora Serverless
│
Response ← ─ ─ ─ ─ ─ ─ ┘
Best Practices
- API Gateway HTTP API (not REST API) — cheaper, faster, simpler for most cases
- One Lambda per route (single responsibility) — not a monolith Lambda
- Keep Lambda warm — use Provisioned Concurrency for latency-sensitive endpoints
- DynamoDB for simple access patterns — scales with traffic, no connection pooling
- Aurora Serverless v2 for complex queries — but use RDS Proxy to manage connections
When to Choose HTTP API vs REST API
| Feature | HTTP API | REST API |
|---|---|---|
| Cost | $1.00/million requests | $3.50/million requests |
| Latency | Lower (~10ms added) | Higher (~30ms added) |
| Features | JWT auth, CORS, Lambda integration | WAF, usage plans, API keys, caching, request validation |
| Choose when | Standard APIs, cost-sensitive | Need WAF, throttling plans, request transforms |
Pattern 2: Async Event Processing
Events trigger Lambda. Processing happens independently of the caller.
S3 Upload ──→ Lambda: process image ──→ S3: store thumbnail
SQS Message ──→ Lambda: process order ──→ DynamoDB: update status
EventBridge ──→ Lambda: handle event ──→ SNS: send notification
Best Practices
- Always use Dead Letter Queues (DLQ) — failed events go to DLQ, not lost
- Design for idempotency — events may be delivered more than once
- Batch processing — SQS Lambda trigger processes up to 10 messages per invocation (cost efficient)
- Set reserved concurrency — prevent one function from consuming all account concurrency
- Use event filtering — Lambda event source filtering reduces invocations (cheaper + simpler)
Event Source Filtering Example
{
"FilterCriteria": {
"Filters": [
{
"Pattern": "{\"body\": {\"status\": [\"critical\"]}}"
}
]
}
}
Lambda only invokes for messages where body.status == "critical". Other messages are filtered out at the service level (free).
Pattern 3: Workflow Orchestration (Step Functions)
For multi-step processes with branching, retries, and error handling.
Step Function:
├── Validate input
├── Process payment (Lambda)
│ ├── Success → Reserve inventory (Lambda)
│ └── Failure → Notify customer (SNS) → End
├── Ship order (Lambda)
├── Wait 7 days
└── Send follow-up email (Lambda)
Step Functions: Express vs Standard
| Feature | Standard | Express |
|---|---|---|
| Duration | Up to 1 year | Up to 5 minutes |
| Pricing | Per state transition ($0.025/1000) | Per execution + duration |
| Execution model | Exactly-once | At-least-once |
| History | Full execution history (90 days) | CloudWatch Logs only |
| Use case | Long-running workflows, human approval | High-volume, short processing (ETL, transforms) |
Direct Service Integrations (Skip Lambda)
Step Functions can call 200+ AWS services directly without Lambda:
{
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:putItem",
"Parameters": {
"TableName": "Orders",
"Item": {
"orderId": {"S.$": "$.orderId"},
"status": {"S": "confirmed"}
}
}
}
No Lambda needed — Step Functions writes to DynamoDB directly. Cheaper, fewer moving parts, lower latency.
Rule: If your Lambda only calls one AWS API — replace it with a direct integration.
Pattern 4: Streaming / Real-Time Processing
For continuous data ingestion and processing.
IoT Devices ──→ Kinesis ──→ Lambda (real-time) ──→ DynamoDB
│
└──→ Firehose ──→ S3 (data lake)
Best Practices
- Kinesis for ordering and replay — Lambda for real-time processing
- Firehose for batched delivery — no code needed for S3/Redshift/OpenSearch
- Tumbling windows — Lambda aggregates over time windows natively
- Bisect on error — Kinesis + Lambda can split failed batches to isolate the bad record
Pattern 5: Fan-Out / Scatter-Gather
One trigger spawns many parallel processes, results are aggregated.
API → Step Function (Distributed Map):
├── Process item 1 (Lambda)
├── Process item 2 (Lambda)
├── Process item 3 (Lambda)
└── ... (10,000 concurrent)
→ Aggregate results → Response
Step Functions Distributed Map processes millions of items with up to 10,000 concurrent executions. Use for:
- Batch processing large datasets from S3
- Parallel API calls to external services
- Large-scale data transformation
Pattern 6: GraphQL API (AppSync)
For applications needing flexible, client-driven queries.
Client → AppSync → Resolvers:
├── DynamoDB (direct resolver, no Lambda)
├── Lambda (complex logic)
├── Aurora (SQL queries)
└── HTTP (external APIs)
AppSync advantages over API Gateway + Lambda:
- Client fetches exactly what it needs (no over-fetching)
- Real-time subscriptions (WebSocket) built in
- Direct DynamoDB/Aurora resolvers (no Lambda needed for CRUD)
- Caching built in
Pattern 7: Scheduled Tasks
Replace cron servers with serverless scheduling.
EventBridge Scheduler → Lambda: run cleanup
EventBridge Rule (rate/cron) → Lambda: generate report
Step Functions Wait → Lambda: send reminder
EventBridge Scheduler vs EventBridge Rules
| Feature | Scheduler | Rules |
|---|---|---|
| One-time events | ✅ (at specific time) | ❌ |
| Timezone support | ✅ (handles DST) | ❌ (UTC only) |
| Scale | Millions of schedules | Limited rules per bus |
| Use case | Per-entity schedules (user reminders) | System-wide recurring jobs |
Anti-Patterns: What NOT to Do
Anti-Pattern 1: Lambda Monolith
The mistake: Putting your entire Express/Flask app inside one Lambda function.
❌ BAD: Single Lambda handles ALL routes
/users, /orders, /products, /admin → one 50MB Lambda
✅ GOOD: One Lambda per route (or per domain)
/users → users-handler
/orders → orders-handler
Why it fails: Cold starts scale with package size. One change requires redeploying everything. No independent scaling per endpoint.
Anti-Pattern 2: Lambda Calling Lambda (Synchronous Chain)
The mistake: Lambda A calls Lambda B which calls Lambda C, all synchronously.
❌ BAD:
Lambda A → invoke → Lambda B → invoke → Lambda C
(paying for A's time while waiting for B and C)
✅ GOOD:
Step Functions: A → B → C (orchestrated, not nested)
Or: A → SQS → B → SQS → C (async, decoupled)
Why it fails: You pay for idle time while waiting. Retry logic becomes complex. Timeouts cascade. Use Step Functions or async messaging instead.
Anti-Pattern 3: Recursive Lambda
The mistake: Lambda invokes itself (or triggers a loop).
❌ DANGEROUS:
Lambda → writes to S3 → triggers same Lambda → writes to S3 → ...
(infinite loop = infinite bill)
Fix: Use separate buckets for input/output, or use event source filtering to exclude your own writes.
Anti-Pattern 4: VPC Lambda Without Need
The mistake: Putting Lambda in a VPC "for security" when it doesn't access VPC resources.
Why it fails: VPC Lambda has cold start overhead (ENI creation). If Lambda only calls DynamoDB, S3, or external APIs — it doesn't need VPC. Use VPC only when accessing RDS, ElastiCache, or private EC2 services.
Anti-Pattern 5: Over-Orchestration
The mistake: Using Step Functions for a simple sequential call that could be a direct Lambda + SDK call.
❌ OVER-ENGINEERED:
Step Function → Lambda (validate) → Lambda (save to DDB)
(3 resources for what one Lambda could do)
✅ APPROPRIATE:
Lambda: validate + save to DDB (if it's simple sequential logic)
Rule: Use Step Functions when you need branching, retries, parallel execution, wait states, or error handling across multiple services. Don't use it for simple A→B flows.
Anti-Pattern 6: Ignoring Cold Starts in Latency-Sensitive Paths
The mistake: Using Lambda for a user-facing API with p99 latency SLA of <100ms without addressing cold starts.
Cold start impact:
- Python/Node.js: 100-300ms
- Java/.NET: 500-3000ms
- VPC Lambda: adds 200-500ms on top
Fix:
- Provisioned Concurrency (pre-warm instances)
- Lambda SnapStart (Java only — restore from checkpoint)
- Keep Lambda small (fewer dependencies = faster cold start)
- Or use Fargate for latency-critical paths with predictable traffic
Anti-Pattern 7: No Concurrency Limits
The mistake: No reserved concurrency on Lambdas in a shared account.
What happens: One function handling a burst consumes all 1,000 concurrent executions → other functions throttled → cascading failures.
Fix: Set reserved concurrency on critical functions. Set account-level concurrency limits on non-critical batch functions.
When Serverless Fits vs When It Doesn't
Serverless Wins
| Characteristic | Why |
|---|---|
| Variable/unpredictable traffic | Scales to zero, no idle cost |
| Event-driven workloads | Natural fit for trigger-based processing |
| Rapid prototyping | Ship in hours, not days |
| Per-request cost model preferred | Pay only for what you use |
| Team wants zero infrastructure ops | No patching, scaling, or capacity planning |
Serverless Doesn't Fit
| Characteristic | Why | Alternative |
|---|---|---|
| Consistent high traffic (always on) | Fargate/EC2 is cheaper at high utilization | Fargate |
| Execution > 15 minutes | Lambda timeout limit | ECS tasks / Step Functions |
| Persistent connections (WebSocket server, gRPC stream) | Lambda is request/response | Fargate / EC2 |
| Cold start unacceptable (< 50ms p99) | Lambda can't guarantee this | Fargate / EC2 with ALB |
| GPU / specialized hardware | Lambda doesn't support GPU | EC2 |
| Large deployment package (> 250MB) | Lambda size limits | Containers on Fargate |
Cost Optimization Patterns
| Pattern | Savings |
|---|---|
| ARM (Graviton) Lambda | 20% cheaper, often 10-30% faster |
| Increase memory (reduce duration) | Often cheaper: 256MB × 400ms costs same as 512MB × 180ms |
| Batch SQS messages (10 per invocation) | 10x fewer invocations |
| Direct integrations (skip Lambda) | No Lambda cost for simple pass-through |
| Event filtering | Reduce unnecessary invocations |
| Provisioned Concurrency (only for latency needs) | ⚠️ Adds cost — use only where needed |
Summary
Serverless on AWS works when you follow these principles:
- Compose from managed services — Lambda is glue, not the application
- Single responsibility — one function per task, not monolith Lambdas
- Async by default — use SQS/EventBridge between services, not synchronous chains
- Step Functions for orchestration — don't build state machines in Lambda code
- Direct integrations — if Lambda just calls one AWS API, remove it
- Design for failure — DLQs everywhere, idempotent handlers, circuit breakers
- Know when it doesn't fit — persistent connections, GPU, >15 min, cold-start-sensitive paths → use Fargate
The best serverless architectures look nothing like traditional three-tier apps shrunk into Lambda. They're composed of events, queues, state machines, and managed services with Lambda connecting the dots.
Alpesh Kumbhare is an AWS Architect at Atos, specializing in AWS serverless architecture and cloud infrastructure automation. Connect on LinkedIn.
Top comments (0)