Serverless Architecture: When to Use and When to Avoid
Serverless has been hyped as the future of computing, but is it right for your use case? After building serverless applications and migrating away from serverless, I've learned that it's powerful but not a silver bullet. Let me share when it makes sense and when it doesn't.
What is Serverless?
Serverless doesn't mean "no servers"—it means you don't manage servers. The cloud provider handles:
- Server provisioning
- Scaling
- Maintenance
- Patching
You just write code.
Serverless Platforms
AWS Lambda
// Lambda function
exports.handler = async (event) => {
const { name } = JSON.parse(event.body);
return {
statusCode: 200,
body: JSON.stringify({
message: `Hello, ${name}!`
})
};
};
Vercel/Netlify Functions
// API route (Vercel)
export default function handler(req, res) {
res.status(200).json({ message: 'Hello World' });
}
Cloudflare Workers
// Cloudflare Worker
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
return new Response('Hello World', {
headers: { 'content-type': 'text/plain' }
});
}
When Serverless Makes Sense
1. Event-Driven Workflows
// Process uploaded image
exports.handler = async (event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = record.s3.object.key;
// Process image
await processImage(bucket, key);
}
};
Perfect for: File processing, webhooks, scheduled tasks
2. API Endpoints
// REST API endpoint
exports.handler = async (event) => {
const { httpMethod, path, body } = event;
if (httpMethod === 'GET' && path === '/users') {
const users = await getUsers();
return {
statusCode: 200,
body: JSON.stringify(users)
};
}
if (httpMethod === 'POST' && path === '/users') {
const user = await createUser(JSON.parse(body));
return {
statusCode: 201,
body: JSON.stringify(user)
};
}
return { statusCode: 404 };
};
Perfect for: REST APIs, GraphQL endpoints
3. Scheduled Tasks
// Cron job (runs every hour)
exports.handler = async (event) => {
// Clean up old data
await cleanupOldRecords();
// Send reports
await sendDailyReports();
};
Perfect for: Scheduled jobs, data cleanup, reports
4. Low-Traffic Applications
Perfect for: MVPs, prototypes, side projects
When to Avoid Serverless
1. Long-Running Processes
// Bad: Processing large dataset
exports.handler = async (event) => {
// Lambda timeout: 15 minutes max
// This might timeout
await processLargeDataset(data);
};
Problem: Lambda has timeout limits (15 minutes on AWS)
Solution: Use containers or traditional servers
2. High-Performance Requirements
Problem: Cold starts add latency
Solution: Use always-on servers for consistent performance
3. Complex State Management
Problem: Stateless by design
Solution: Use traditional servers or external state storage
4. Cost at Scale
Problem: Can be expensive at high volume
Solution: Calculate costs, compare with alternatives
Serverless Patterns
1. API Gateway + Lambda
// API Gateway routes to Lambda
// GET /users → Lambda function
exports.handler = async (event) => {
const users = await getUsers();
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify(users)
};
};
2. Event-Driven
// S3 upload triggers Lambda
exports.handler = async (event) => {
for (const record of event.Records) {
if (record.eventName.startsWith('ObjectCreated')) {
await processUpload(record.s3);
}
}
};
3. Step Functions
// Orchestrate multiple Lambdas
{
"StartAt": "ProcessOrder",
"States": {
"ProcessOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:processOrder",
"Next": "SendEmail"
},
"SendEmail": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:sendEmail",
"End": true
}
}
}
Cold Start Problem
What is Cold Start?
When a Lambda hasn't been invoked recently, it needs to:
- Initialize runtime
- Load dependencies
- Execute code
This adds latency (100ms - 5s+).
Mitigating Cold Starts
// Keep connections outside handler
const db = await connectToDatabase(); // Outside handler
exports.handler = async (event) => {
// Reuse connection
const users = await db.query('SELECT * FROM users');
return users;
};
Strategies:
- Provisioned concurrency - Keep functions warm
- Minimize dependencies - Smaller bundles = faster starts
- Use connection pooling - Reuse connections
Cost Considerations
Pay Per Use
Advantages:
- No cost when not in use
- Scales automatically
- Pay only for what you use
Disadvantages:
- Can be expensive at scale
- Cold starts cost money
- Data transfer costs
Cost Calculation
// Example: 1M requests/month
// Lambda: $0.20 per 1M requests
// Duration: 100ms average, 512MB memory
// Cost: ~$10/month
// Compare to: EC2 instance
// t3.small: $15/month (always on)
// At low traffic: Serverless cheaper
// At high traffic: EC2 might be cheaper
Best Practices
1. Keep Functions Small
// Good: Single responsibility
exports.handler = async (event) => {
return await processOrder(event.orderId);
};
// Bad: Multiple responsibilities
exports.handler = async (event) => {
await processOrder();
await sendEmail();
await updateInventory();
await generateReport();
};
2. Use Environment Variables
// Store configuration
process.env.DATABASE_URL
process.env.API_KEY
process.env.ENVIRONMENT
3. Handle Errors Gracefully
exports.handler = async (event) => {
try {
return await processRequest(event);
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
};
}
};
4. Set Timeouts Appropriately
// Don't set too high (costs money)
// Don't set too low (timeouts)
// Find the right balance
5. Monitor and Log
// Use CloudWatch (AWS) or similar
console.log('Function invoked', { event });
console.error('Error occurred', { error });
Real-World Example
Project: Image processing service
Requirements:
- Process images on upload
- Variable traffic (0-1000/day)
- Need to scale automatically
Serverless Solution:
// S3 upload → Lambda trigger
exports.handler = async (event) => {
for (const record of event.Records) {
const imageKey = record.s3.object.key;
// Resize images
await resizeImage(imageKey, [800, 400, 200]);
// Generate thumbnails
await generateThumbnails(imageKey);
// Update database
await updateImageMetadata(imageKey);
}
};
Result:
- Cost: $5/month (vs $50/month for always-on server)
- Scales automatically
- No server management
Hybrid Approach
You don't have to go all-in on serverless:
// Traditional server for main API
app.get('/api/users', getUsers);
// Serverless for specific tasks
// - Image processing
// - Email sending
// - Scheduled jobs
Conclusion
Serverless is powerful but not always the right choice. Use it when:
- Event-driven - Perfect fit
- Variable traffic - Cost-effective
- Simple functions - Easy to manage
- No long-running processes - Fits constraints
Avoid it when:
- Consistent high traffic - Might be expensive
- Long-running processes - Timeout limits
- Complex state - Stateless by design
- Performance critical - Cold starts matter
Remember: Serverless is a tool, not a goal. Choose the right tool for the job.
What serverless experiences have you had? What worked and what didn't?
Related Posts
GraphQL vs REST: Making the Right API Choice in 2025
A comprehensive comparison of GraphQL and REST APIs in 2025. Learn when to use each approach, their trade-offs, and how to make the right decision for your project.
Microservices vs Monoliths: When to Choose What in 2024
A practical guide to choosing between microservices and monolithic architectures. Learn when each approach makes sense, common pitfalls, and how to make the right decision for your project.
Modern Authentication: OAuth 2.0, JWT, and Session Management
Master modern authentication patterns including OAuth 2.0, JWT tokens, and session management. Learn when to use each approach and how to implement them securely.
Event-Driven Architecture: Patterns and Best Practices
Learn how to build scalable, decoupled systems using event-driven architecture. Discover patterns, message brokers, and real-world implementation strategies.
Cost Optimization in Cloud Infrastructure: Real-World Strategies
Learn practical strategies to reduce cloud infrastructure costs without sacrificing performance or reliability. Real techniques that have saved thousands of dollars.
Data Modeling for Scalable Applications: Normalization vs Denormalization
Learn when to normalize and when to denormalize your database schema. Master the art of data modeling for applications that scale to millions of users.