Serverless Architecture: When to Use and When to Avoid

Serverless Architecture: When to Use and When to Avoid

BySanjay Goraniya
3 min read
Share:

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

Code
// Lambda function
exports.handler = async (event) => {
  const { name } = JSON.parse(event.body);
  
  return {
    statusCode: 200,
    body: JSON.stringify({
      message: `Hello, ${name}!`
    })
  };
};

Vercel/Netlify Functions

Code
// API route (Vercel)
export default function handler(req, res) {
  res.status(200).json({ message: 'Hello World' });
}

Cloudflare Workers

Code
// 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

Code
// 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

Code
// 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

Code
// 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

Code
// 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

Code
// 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

Code
// 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

Code
// 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:

  1. Initialize runtime
  2. Load dependencies
  3. Execute code

This adds latency (100ms - 5s+).

Mitigating Cold Starts

Code
// 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

Code
// 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

Code
// 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

Code
// Store configuration
process.env.DATABASE_URL
process.env.API_KEY
process.env.ENVIRONMENT

3. Handle Errors Gracefully

Code
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

Code
// Don't set too high (costs money)
// Don't set too low (timeouts)
// Find the right balance

5. Monitor and Log

Code
// 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:

Code
// 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:

Code
// 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?

Share:

Related Posts