September 15, 2025Serverless

AWS Serverless Best Practices

Ten battle-tested patterns for building secure, scalable AWS Lambda apps — cold starts, IAM least privilege, observability, and the mistakes that bite you in production.

After deploying hundreds of serverless applications to production, we've learned what works and what doesn't. These battle-tested best practices will help you build scalable, maintainable, and cost-effective serverless applications on AWS.

1. Design for Statelessness

Lambda functions are ephemeral by design. Never store state in function memory or the filesystem beyond the execution context.

// ❌ Bad: Storing state in memory
let userCache = {};

exports.handler = async (event) => {
    userCache[event.userId] = event.data;
    // This cache will be lost when the container is recycled
};

// ✅ Good: Using external storage
const DynamoDB = require('aws-sdk/clients/dynamodb');
const client = new DynamoDB.DocumentClient();

exports.handler = async (event) => {
    await client.put({
        TableName: 'UserCache',
        Item: { userId: event.userId, data: event.data }
    }).promise();
};

2. Optimize Cold Starts

Cold starts can impact user experience. Minimize them by keeping functions small, using provisioned concurrency for critical paths, and optimizing dependencies. Learn about the cost implications in our Lambda cost calculator.

  • Keep deployment packages under 50MB
  • Use tree-shaking to remove unused code
  • Initialize SDK clients outside the handler
  • Consider using Lambda Layers for shared dependencies
Pro Tip: For Node.js functions, use webpack or esbuild to bundle and minimize your code. This can reduce cold start times by up to 70%.

3. Implement Proper Error Handling

Robust error handling is crucial for production reliability. Implement retry logic, use dead letter queues, and monitor failures.

exports.handler = async (event) => {
    try {
        const result = await processData(event);
        return {
            statusCode: 200,
            body: JSON.stringify(result)
        };
    } catch (error) {
        console.error('Processing failed:', error);

        // Send to DLQ for analysis
        await sendToDeadLetterQueue(event, error);

        // Return appropriate error response
        if (error.retryable) {
            throw error; // Let Lambda retry
        }

        return {
            statusCode: error.statusCode || 500,
            body: JSON.stringify({
                error: 'Processing failed',
                requestId: context.requestId
            })
        };
    }
};

4. Use Environment Variables Wisely

Environment variables are great for configuration, but don't store secrets directly. Use AWS Systems Manager Parameter Store or Secrets Manager.

Warning: Environment variables are visible in the Lambda console and CloudFormation templates. Never store sensitive data like API keys or passwords directly in environment variables.

5. Implement Structured Logging

Use structured JSON logging for better observability. This makes it easier to query logs in CloudWatch Insights.

const log = (level, message, meta = {}) => {
    console.log(JSON.stringify({
        timestamp: new Date().toISOString(),
        level,
        message,
        requestId: context.requestId,
        ...meta
    }));
};

// Usage
log('INFO', 'Processing order', {
    orderId: '12345',
    amount: 99.99
});

6. Set Appropriate Timeouts and Memory

Right-size your functions based on actual usage. Monitor with CloudWatch and X-Ray to find the optimal configuration. See our EC2 vs Serverless cost analysis for performance comparisons.

  • Start with 3-second timeout and 512MB memory
  • Use AWS Lambda Power Tuning to find optimal settings
  • Remember: More memory = More CPU power
  • Set timeouts slightly above your p99 execution time

7. Use Async/Await Properly

Leverage async/await for cleaner code and better error handling. Avoid callback hell and properly handle promise rejections.

// ✅ Good: Parallel execution when possible
exports.handler = async (event) => {
    const [userData, orderData, inventoryData] = await Promise.all([
        fetchUserData(event.userId),
        fetchOrderData(event.orderId),
        checkInventory(event.items)
    ]);

    return processOrder(userData, orderData, inventoryData);
};

8. Implement Idempotency

Make your functions idempotent to handle retries safely. Use idempotency keys and check for duplicate processing.

Functions should produce the same result regardless of how many times they're called with the same input. This is critical for event-driven architectures.

9. Monitor and Alert Proactively

Set up comprehensive monitoring with CloudWatch Alarms for key metrics:

  • Error rate > 1%
  • Throttling occurrences
  • Duration approaching timeout
  • Concurrent executions near limit
  • Dead letter queue messages

10. Use Infrastructure as Code

Always define your serverless infrastructure using IaC tools like SAM, CDK, or Terraform. This ensures consistency and enables CI/CD.

# SAM Template Example
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 30
    MemorySize: 512
    Runtime: nodejs18.x
    Tracing: Active
    Environment:
      Variables:
        STAGE: !Ref Stage

Resources:
  ProcessOrderFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./src
      Handler: orders.handler
      ReservedConcurrentExecutions: 10
      DeadLetterQueue:
        Type: SQS
        TargetArn: !GetAtt DeadLetterQueue.Arn

Bonus: Cost Optimization Tips

Want to see real cost savings in action? Read our case study on cutting AWS costs by 85%.

  • Use Step Functions for orchestration instead of chaining Lambdas
  • Implement caching with ElastiCache or DynamoDB to reduce compute
  • Use SQS for buffering to smooth out traffic spikes
  • Enable compression for API Gateway responses
  • Set up cost alerts to catch unexpected usage early

Planning a build?

We're a small app development studio that ships production software — not slide decks. Get an honest estimate with no strings attached.

Get a Free Project Estimate