Serverless isn't always the answer—here's how to know when it is
When Serverless Makes Sense
| scenario | serverless | traditional |
|---|---|---|
| Variable/unpredictable traffic | Excellent | Wasteful |
| Consistent high traffic | Expensive | Cost-effective |
| Quick MVP/prototype | Excellent | Slower |
| Long-running processes | Limited (15 min max) | Better |
| Event-driven workloads | Excellent | Overkill |
| Real-time websockets | Possible but complex | Simpler |
AWS Serverless Architecture
Typical Serverless Stack:
Compute:
- AWS Lambda: Function execution
- Step Functions: Workflow orchestration
API:
- API Gateway: REST/HTTP APIs
- AppSync: GraphQL APIs
Data:
- DynamoDB: NoSQL database
- Aurora Serverless: Relational database
- S3: Object storage
Events:
- EventBridge: Event routing
- SQS: Message queuing
- SNS: Pub/sub messaging
Auth:
- Cognito: User authentication
Lambda Function Example
// Example: API endpoint with Lambda
import { APIGatewayProxyHandler } from 'aws-lambda';
import { DynamoDB } from 'aws-sdk';
const dynamodb = new DynamoDB.DocumentClient();
export const handler: APIGatewayProxyHandler = async (event) => {
try {
const { id } = event.pathParameters || {};
if (!id) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'ID required' })
};
}
const result = await dynamodb.get({
TableName: process.env.TABLE_NAME!,
Key: { id }
}).promise();
if (!result.Item) {
return {
statusCode: 404,
body: JSON.stringify({ error: 'Not found' })
};
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify(result.Item)
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
};
}
};
Cost Analysis
Serverless Pricing Model
// Lambda pricing breakdown
const lambdaPricing = {
requests: 0.20, // per 1M requests
compute: 0.0000166667, // per GB-second
// Example: 1M requests/month, 500ms avg, 512MB memory
example: {
requests: 1_000_000,
avgDurationMs: 500,
memoryMB: 512,
// Calculate
requestCost: 1 * 0.20, // $0.20
computeGBSeconds: 1_000_000 * 0.5 * 0.5, // 250,000 GB-seconds
computeCost: 250_000 * 0.0000166667, // $4.17
totalMonthly: 4.37 // $4.37/month
}
};
| traffic | serverlessCost | ec2tMicro | winner | ec2tSmall | ec2tMedium |
|---|---|---|---|---|---|
| 100K requests/month | $0.50 | $8.50 | Serverless | — | — |
| 1M requests/month | $4.50 | $8.50 | Serverless | — | — |
| 10M requests/month | $45 | — | EC2 | $17 | — |
| 100M requests/month | $450 | — | EC2 | — | $34 |
Key Insight: Serverless is cost-effective for variable/low traffic. Once you exceed ~5-10M consistent requests/month, traditional servers often become cheaper.
Common Serverless Patterns
Pattern 1: API Backend
Architecture:
Client -> API Gateway -> Lambda -> DynamoDB
Use Case: REST API for web/mobile apps
Pros: Auto-scaling, pay-per-use, zero servers
Cons: Cold starts, 29-second timeout (API Gateway)
Pattern 2: Event Processing
Architecture:
S3 Upload -> Lambda -> Process -> Store Result
Use Case: Image processing, file conversion
Pros: Automatic triggers, parallel processing
Cons: 15-minute timeout, temp storage limits
Pattern 3: Scheduled Tasks
Architecture:
EventBridge (cron) -> Lambda -> External APIs
Use Case: Data sync, reports, cleanup tasks
Pros: No server maintenance, precise scheduling
Cons: 15-minute limit per execution
Serverless Best Practices
// Best Practice 1: Minimize cold starts
// Initialize outside handler
const dynamodb = new DynamoDB.DocumentClient(); // ✅ Outside
let cachedConfig: Config | null = null;
export const handler = async (event) => {
// Reuse connections and cached data
if (!cachedConfig) {
cachedConfig = await loadConfig();
}
// Handler logic...
};
// Best Practice 2: Use appropriate memory
// More memory = more CPU = faster execution
// Sometimes 1024MB is cheaper than 512MB (runs faster)
// Best Practice 3: Keep functions focused
// One function per responsibility
// ❌ handleEverything()
// ✅ createUser(), updateUser(), deleteUser()
When NOT to Use Serverless
Avoid Serverless For:
- • Long-running processes (over 15 minutes)
- • Consistent high-traffic applications
- • Applications requiring persistent connections
- • Workloads with predictable, steady load
- • Applications with strict latency requirements
Migration Path
## Gradual Migration to Serverless
Phase 1: New Features
- Build new features as Lambda functions
- Keep existing infrastructure
- Learn and iterate
Phase 2: Extract Services
- Identify stateless components
- Migrate one service at a time
- Monitor costs and performance
Phase 3: Full Migration (if beneficial)
- Migrate remaining components
- Decommission servers
- Optimize for serverless patterns
Need help designing your serverless architecture?
We help companies build cost-effective, scalable serverless solutions on AWS.
Get Architecture Advice