Skip to content
All essays
CraftMarch 20, 202516 min

Serverless Cost Optimization: Reduce Your Cloud Bills

Master serverless cost optimization. Learn strategies to reduce cloud bills, monitor spending, and optimize performance in 2025.

Ü
Ümit Uz
Mobile & Full Stack Developer

Understanding Serverless Costs

Serverless computing promises pay-per-use pricing, but costs can spiral without proper optimization. In 2025, successful companies implement comprehensive cost management strategies that save 40-70% on their serverless bills.

The Hidden Costs of Serverless:

1. Request Overhead:
   - 10M API calls = $20.00
   - With optimization: 5M calls = $10.00

2. Compute Time:
   - Average 500ms × 10M calls = $83.33
   - Optimized 200ms × 5M calls = $16.67

3. Data Transfer:
   - 1TB outbound = $90.00
   - Cached responses (100GB) = $9.00

Monthly Savings: Up to 70%

Cost Analysis Framework

AWS Lambda Cost Breakdown

Request Costs:
- First 1M requests/month: Free
- Next 49M requests: $0.20 per 1M
- Over 50M requests: $0.20 per 1M

Compute Costs:
- 128MB, 1 second: $0.0000002083
- 1024MB, 10 seconds: $0.0000166667

Data Transfer:
- First 100GB/month: Free
- 100GB-10TB: $0.09 per GB

Example API Endpoint:
100 requests/second × 2,592,000 seconds/month = 259.2M requests

Unoptimized:
- Requests: $51.84
- Compute (1024MB, 2s): $86.40
- Total: $138.24/month

Optimized:
- Requests: $25.92 (50% reduction)
- Compute (256MB, 0.5s): $5.40
- Total: $31.32/month

Savings: $106.92/month (77% reduction)

Optimization Strategies

1. Right-Size Memory Allocation

typescript
// tools/memoryOptimizer.ts
import { LambdaClient, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda';

const lambda = new LambdaClient({});

interface MemoryTestResult {
  memorySize: number;
  duration: number;
  cost: number;
}

export async function findOptimalMemory(
  functionName: string
): Promise<MemoryTestResult[]> {
  const testSizes = [128, 256, 512, 1024, 1536, 2048, 3008];
  const results: MemoryTestResult[] = [];

  for (const memorySize of testSizes) {
    // Update function memory
    await lambda.send(new UpdateFunctionConfigurationCommand({
      FunctionName: functionName,
      MemorySize: memorySize
    }));

    // Warm up and test
    await warmUpFunction(functionName);
    const durations = await runBenchmark(functionName, 100);
    const avgDuration = durations.reduce((a, b) => a + b) / durations.length;

    // Calculate cost per million invocations
    const cost = (memorySize / 1024) * avgDuration * 0.0000166667;

    results.push({
      memorySize,
      duration: avgDuration,
      cost: cost * 1000000  // Cost per million
    });
  }

  // Find optimal (lowest cost)
  results.sort((a, b) => a.cost - b.cost);

  return results;
}

async function warmUpFunction(functionName: string) {
  // Invoke function 5 times to warm up
  for (let i = 0; i < 5; i++) {
    await invokeLambda(functionName, {});
  }
}

async function runBenchmark(functionName: string, iterations: number) {
  const durations: number[] = [];

  for (let i = 0; i < iterations; i++) {
    const start = Date.now();
    await invokeLambda(functionName, {});
    durations.push(Date.now() - start);
  }

  return durations;
}

async function invokeLambda(functionName: string, payload: any) {
  // Lambda invocation logic
}

2. Implement Intelligent Caching

typescript
// patterns/responseCaching.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb';

const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));

interface CacheEntry {
  data: any;
  timestamp: number;
  ttl: number;
}

export function withCache(
  handler: (event: APIGatewayProxyEvent) => Promise<APIGatewayProxyResult>,
  options: {
    ttl?: number;  // Time to live in seconds
    keyGenerator?: (event: APIGatewayProxyEvent) => string;
  } = {}
) {
  const { ttl = 300, keyGenerator } = options;

  return async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const cacheKey = keyGenerator
      ? keyGenerator(event)
      : generateCacheKey(event);

    // Check cache
    const cached = await docClient.send(new GetCommand({
      TableName: 'ResponseCache',
      Key: { cacheKey }
    }));

    if (cached.Item) {
      const entry = cached.Item as unknown as CacheEntry;
      const age = (Date.now() - entry.timestamp) / 1000;

      if (age < entry.ttl) {
        return {
          statusCode: 200,
          headers: {
            'X-Cache': 'HIT',
            'Age': age.toString()
          },
          body: JSON.stringify(entry.data)
        };
      }
    }

    // Cache miss - execute handler
    const result = await handler(event);

    // Only cache successful GET requests
    if (event.httpMethod === 'GET' && result.statusCode === 200) {
      await docClient.send(new PutCommand({
        TableName: 'ResponseCache',
        Item: {
          cacheKey,
          data: JSON.parse(result.body),
          timestamp: Date.now(),
          ttl: ttl
        }
      }));
    }

    result.headers = {
      ...result.headers,
      'X-Cache': 'MISS'
    };

    return result;
  };
}

function generateCacheKey(event: APIGatewayProxyEvent): string {
  return `cache:${event.pathParameters || ''}:${event.body || ''}`;
}

// Usage
export const handler = withCache(async (event) => {
  // Expensive operation
  const data = await fetchExpensiveData();

  return {
    statusCode: 200,
    body: JSON.stringify(data)
  };
}, { ttl: 600 });

3. Use Lambda Layers for Shared Code

bash
# Create a layer for common dependencies
mkdir -p layer/nodejs
cd layer/nodejs
npm install axios lodash moment

# Package the layer
cd ../..
zip -r common-dependencies.zip nodejs/

# Publish the layer
aws lambda publish-layer-version \
  --layer-name common-dependencies \
  --description "Shared dependencies for Lambda functions" \
  --zip-file fileb://common-dependencies.zip \
  --compatible-runtimes nodejs20.x nodejs18.x \
  --compatible-architectures x86_64 arm64

# Output: LayerVersionArn: arn:aws:lambda:region:account:layer:common-dependencies:version
yaml
# serverless.yml with layers
layers:
  common:
    path: layer
    description: Shared dependencies
    compatibleRuntimes:
      - nodejs20.x
      - nodejs18.x

functions:
  api:
    handler: src/api.handler
    layers:
      - { Ref: CommonLambdaLayer }
    # No need to bundle axios, lodash, moment
    package:
      exclude:
        - node_modules/**

4. Optimize Bundle Sizes

typescript
// esbuild.config.js
import esbuild from 'esbuild';

export async function build() {
  await esbuild.build({
    entryPoints: ['src/handler.ts'],
    bundle: true,
    minify: true,
    treeShaking: true,
    sourcemap: true,
    target: 'node20',
    platform: 'node',
    external: [
      'aws-sdk',  // Provided by AWS runtime
      '@aws-sdk/*'  // Provided by AWS runtime
    ],
    outdir: 'dist',
    define: {
      'process.env.NODE_ENV': '"production"'
    },
    // Remove unused exports
    drop: ['console', 'debugger']
  });
}

// webpack.config.js alternative
module.exports = {
  mode: 'production',
  entry: './src/handler.ts',
  target: 'node',
  externals: [
    'aws-sdk',
    /^@aws-sdk\//
  ],
  optimization: {
    minimize: true,
    usedExports: true,
    sideEffects: false
  },
  performance: {
    maxAssetSize: 1000000,  // 1MB
    maxEntrypointSize: 1000000
  }
};

Advanced Cost Optimization

5. Implement Request Batching

typescript
// patterns/requestBatching.ts
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';

const sqs = new SQSClient({});

interface BatchConfig {
  maxSize: number;
  maxWait: number;  // milliseconds
  flushInterval: number;
}

export class RequestBatcher {
  private batch: any[] = [];
  private lastFlush = Date.now();
  private timer?: NodeJS.Timeout;

  constructor(
    private queueUrl: string,
    private config: BatchConfig = {
      maxSize: 10,
      maxWait: 5000,
      flushInterval: 30000
    }
  ) {
    // Auto-flush on interval
    this.timer = setInterval(() => {
      this.flush();
    }, this.config.flushInterval);
  }

  async add(item: any): Promise<void> {
    this.batch.push(item);

    if (this.batch.length >= this.config.maxSize) {
      await this.flush();
    }
  }

  private async flush(): Promise<void> {
    if (this.batch.length === 0) return;

    const items = this.batch.splice(0);

    // Send as batch to SQS
    await sqs.send(new SendMessageCommand({
      QueueUrl: this.queueUrl,
      MessageBody: JSON.stringify({ items }),
      MessageAttributes: {
        BatchSize: {
          DataType: 'Number',
          StringValue: items.length.toString()
        }
      }
    }));

    this.lastFlush = Date.now();
  }

  destroy(): void {
    if (this.timer) {
      clearInterval(this.timer);
    }
  }
}

// Usage in Lambda
const batcher = new RequestBatcher(process.env.BATCH_QUEUE_URL);

export const handler = async (event: any) => {
  // Instead of processing each item individually
  for (const record of event.Records) {
    await batcher.add(record);
  }

  // Batch will auto-flush when full or on interval
  return { processed: event.Records.length };
};

6. Use Provisioned Concurrency Strategically

typescript
// patterns/provisionedConcurrency.ts
interface ProvisionedConfig {
  functionName: string;
  minInstances: number;
  maxInstances: number;
  targetUtilization: number;
}

export async function optimizeProvisionedConcurrency(
  config: ProvisionedConfig
): Promise<void> {
  const metrics = await getFunctionMetrics(config.functionName);
  const avgConcurrency = metrics.averageConcurrentExecutions;
  const peakConcurrency = metrics.peakConcurrentExecutions;

  // Calculate optimal provisioned concurrency
  const minProvisioned = Math.ceil(peakConcurrency * 0.3);
  const maxProvisioned = Math.ceil(peakConcurrency * 0.8);

  // Update alias
  await updateProvisionedConcurrency(config.functionName, {
    minProvisioned,
    maxProvisioned,
    targetUtilization: config.targetUtilization
  });
}

// serverless.yml configuration
functions:
  api:
    handler: src/api.handler
    provisionedConcurrency: 5
    reservedConcurrentExecutions: 100

  # Auto-scaling configuration
  autoScaling:
    - type: 'ProvisionedConcurrency'
      minimum: 5
      maximum: 100
      target: 70  # 70% utilization

7. Optimize Database Queries

typescript
// patterns/databaseOptimization.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
  DynamoDBDocumentClient,
  QueryCommand,
  BatchGetCommand
} from '@aws-sdk/lib-dynamodb';

const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));

// BEFORE: N+1 query problem
export async function getOrdersWithItemsBad(userId: string) {
  const orders = await docClient.send(new QueryCommand({
    TableName: 'Orders',
    KeyConditionExpression: 'userId = :userId',
    ExpressionAttributeValues: {
      ':userId': userId
    }
  }));

  // N+1 queries!
  for (const order of orders.Items || []) {
    const items = await docClient.send(new QueryCommand({
      TableName: 'OrderItems',
      KeyConditionExpression: 'orderId = :orderId',
      ExpressionAttributeValues: {
        ':orderId': order.id
      }
    }));
    order.items = items.Items;
  }

  return orders.Items;
}

// AFTER: Single query with projection
export async function getOrdersWithItemsGood(userId: string) {
  // Use a composite key or GSI
  const result = await docClient.send(new QueryCommand({
    TableName: 'Orders',
    IndexName: 'UserIdCreatedAtIndex',
    KeyConditionExpression: 'userId = :userId',
    ProjectionExpression: 'id, #date, total, items',
    ExpressionAttributeNames: {
      '#date': 'date'
    },
    ExpressionAttributeValues: {
      ':userId': userId
    }
  }));

  return result.Items;
}

// Batch operations for efficiency
export async function batchGetItems(keys: string[]) {
  const chunks = [];
  const CHUNK_SIZE = 100;  // DynamoDB limit

  for (let i = 0; i < keys.length; i += CHUNK_SIZE) {
    chunks.push(keys.slice(i, i + CHUNK_SIZE));
  }

  const results = await Promise.all(
    chunks.map(chunk =>
      docClient.send(new BatchGetCommand({
        RequestItems: {
          ItemsTable: {
            Keys: chunk.map(key => ({ id: key }))
          }
        }
      }))
    )
  );

  return results.flatMap(r => r.Responses?.ItemsTable || []);
}

Monitoring & Alerting

8. Implement Cost Monitoring

typescript
// monitoring/costMonitor.ts
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch';

const cloudwatch = new CloudWatchClient({});

interface CostMetrics {
  invocationCount: number;
  totalDuration: number;
  averageDuration: number;
  estimatedCost: number;
}

export class CostMonitor {
  private invocationCount = 0;
  private totalDuration = 0;
  private memorySize: number;

  constructor(memorySize: number) {
    this.memorySize = memorySize;
  }

  recordInvocation(duration: number): void {
    this.invocationCount++;
    this.totalDuration += duration;
  }

  getMetrics(): CostMetrics {
    const averageDuration = this.totalDuration / this.invocationCount;
    const computeCost = (this.memorySize / 1024) * averageDuration * 0.0000166667;
    const requestCost = this.invocationCount * 0.0000002;
    const estimatedCost = (computeCost + requestCost) * this.invocationCount;

    return {
      invocationCount: this.invocationCount,
      totalDuration: this.totalDuration,
      averageDuration,
      estimatedCost
    };
  }

  async publishMetrics(functionName: string): Promise<void> {
    const metrics = this.getMetrics();

    await cloudwatch.send(new PutMetricDataCommand({
      Namespace: 'ServerlessCosts',
      MetricData: [
        {
          MetricName: 'EstimatedCost',
          Value: metrics.estimatedCost,
          Unit: 'None',
          Dimensions: [
            { Name: 'FunctionName', Value: functionName }
          ]
        },
        {
          MetricName: 'InvocationCount',
          Value: metrics.invocationCount,
          Unit: 'Count',
          Dimensions: [
            { Name: 'FunctionName', Value: functionName }
          ]
        },
        {
          MetricName: 'AverageDuration',
          Value: metrics.averageDuration,
          Unit: 'Milliseconds',
          Dimensions: [
            { Name: 'FunctionName', Value: functionName }
          ]
        }
      ]
    }));
  }
}

// Usage in Lambda
const monitor = new CostMonitor(256);

export const handler = async (event: any) => {
  const start = Date.now();

  try {
    // Business logic
    const result = await processEvent(event);

    monitor.recordInvocation(Date.now() - start);

    return result;
  } finally {
    await monitor.publishMetrics('my-function');
  }
};

9. Set Up Budget Alerts

typescript
// tools/budgetAlerts.ts
import { BudgetsClient, CreateBudgetCommand, CreateNotificationCommand } from '@aws-sdk/client-budgets';

const budgets = new BudgetsClient({});

export async function createBudgetAlert(
  accountId: string,
  limit: number,
  email: string
): Promise<void> {
  // Create budget
  await budgets.send(new CreateBudgetCommand({
    AccountId: accountId,
    Budget: {
      BudgetName: 'ServerlessMonthlyBudget',
      BudgetLimit: {
        Amount: limit.toString(),
        Unit: 'USD'
      },
      TimeUnit: 'MONTHLY',
      BudgetType: 'COST',
      CostFilters: {
        Service: ['AWS Lambda', 'Amazon API Gateway', 'Amazon DynamoDB']
      }
    }
  }));

  // Create notification
  await budgets.send(new CreateNotificationCommand({
    AccountId: accountId,
    BudgetName: 'ServerlessMonthlyBudget',
    Notification: {
      NotificationType: 'ACTUAL',
      ComparisonOperator: 'GREATER_THAN',
      Threshold: 80,  // Alert at 80%
      ThresholdType: 'PERCENTAGE'
    },
    Subscribers: [
      {
        SubscriptionType: 'EMAIL',
        Address: email
      }
    ]
  }));
}

Cost Optimization Checklist

Lambda Functions

  • [ ] Right-size memory allocation
  • [ ] Optimize bundle sizes (<5MB zipped)
  • [ ] Use Lambda Layers for shared code
  • [ ] Implement intelligent caching
  • [ ] Use provisioned concurrency wisely
  • [ ] Set appropriate timeouts
  • [ ] Remove unused functions
  • [ ] Optimize cold start frequency

API Gateway

  • [ ] Use HTTP APIs instead of REST APIs (70% cheaper)
  • [ ] Implement request caching
  • [ ] Use WebSocket APIs for real-time
  • [ ] Enable CloudWatch Logs selectively
  • [ ] Use throttling to prevent abuse
  • [ ] Optimize API endpoint design

DynamoDB

  • [ ] Use on-demand pricing for variable workloads
  • [ ] Implement TTL for expired data
  • [ ] Use GSIs strategically
  • [ ] Optimize query patterns
  • [ ] Enable Point-in-Time Recovery selectively
  • [ ] Use batch operations

General

  • [ ] Monitor costs daily
  • [ ] Set up budget alerts
  • [ ] Review unused resources
  • [ ] Use AWS Organizations for tagging
  • [ ] Implement cost allocation tags
  • [ ] Regular cost reviews

Real-World Cost Savings

Case Study 1: E-commerce API

Before Optimization:
- 10M requests/month
- Average 1s execution time
- 1024MB memory
- Cost: $180/month

Optimizations Applied:
1. Reduced memory to 256MB (faster execution)
2. Implemented response caching
3. Optimized database queries
4. Used HTTP API instead of REST API

After Optimization:
- 4M requests/month (60% reduction from caching)
- Average 300ms execution time
- 256MB memory
- Cost: $35/month

Savings: $145/month (80% reduction)

Case Study 2: Data Processing Pipeline

Before Optimization:
- 1M records/day
- Step Functions orchestration
- Individual Lambda invocations
- Cost: $250/month

Optimizations Applied:
1. Implemented batch processing
2. Used SQS for queuing
3. Increased batch sizes
4. Optimized memory allocation

After Optimization:
- 1M records/day (same volume)
- Batch processing (100 records/batch)
- 10K invocations instead of 1M
- Cost: $45/month

Savings: $205/month (82% reduction)

Tools & Resources

AWS Cost Explorer

bash
# Enable detailed monitoring
aws lambda put-function-concurrency \
  --function-name my-function \
  --reserved-concurrent-executions 100

# Create cost allocation tags
aws resourcegroups tag-resources \
  --resource-arns arn:aws:lambda:region:account:function:my-function \
  --tags Environment=Production,Team=Backend

# View cost breakdown
aws ce get-cost-and-usage \
  --time-period Start=2025-01-01,End=2025-01-31 \
  --granularity MONTHLY \
  --metrics "BlendedCost","UnblendedCost" \
  --group-by Type=DIMENSION,Key=SERVICE

Third-Party Tools

  1. 1Serverless Framework Cost Dashboard

- Real-time cost tracking - Anomaly detection - Optimization recommendations

  1. 1CloudHealth

- Multi-cloud cost management - Budget tracking - Resource optimization

  1. 1Datadog Cloud Cost Management

- Real-time monitoring - Cost allocation - Anomaly alerts

Automation Scripts

typescript
// scripts/costOptimizer.ts
import { LambdaClient, ListFunctionsCommand } from '@aws-sdk/client-lambda';

const lambda = new LambdaClient({});

export async function optimizeAllFunctions(): Promise<void> {
  const functions = await lambda.send(new ListFunctionsCommand({}));

  for (const func of functions.Functions || []) {
    console.log(`Analyzing ${func.FunctionName}...`);

    // Check memory allocation
    const currentMemory = func.MemorySize || 128;

    // Run memory optimization test
    const optimal = await findOptimalMemory(func.FunctionName!);

    if (optimal[0].memorySize !== currentMemory) {
      console.log(`Recommendation: Change memory from ${currentMemory}MB to ${optimal[0].memorySize}MB`);
      console.log(`Potential savings: ${calculateSavings(currentMemory, optimal[0]).toFixed(2)}%`);
    }
  }
}

function calculateSavings(current: number, optimal: any): number {
  return ((current - optimal.memorySize) / current) * 100;
}

Best Practices Summary

Development

  1. 1Start with cost in mind: Design for cost from day one
  2. 2Monitor continuously: Track costs in real-time
  3. 3Test for cost: Include cost in your test suite
  4. 4Document decisions: Keep track of cost-related decisions

Operations

  1. 1Review regularly: Monthly cost reviews
  2. 2Set alerts: Proactive cost alerts
  3. 3Optimize iteratively: Continuous improvement
  4. 4Share knowledge: Team cost awareness

Architecture

  1. 1Use right tools: Choose appropriate services
  2. 2Implement caching: Reduce invocations
  3. 3Batch operations: Lower overhead
  4. 4Scale efficiently: Match capacity to demand

Conclusion

Serverless cost optimization is not a one-time task but an ongoing process. By implementing these strategies:

  • Right-sizing resources: Match capacity to needs
  • Intelligent caching: Reduce redundant work
  • Efficient patterns: Batch, layer, optimize
  • Continuous monitoring: Track and alert

Companies achieve 40-80% cost savings while maintaining or improving performance.

Start with the biggest wins (memory sizing, caching, HTTP APIs) and iterate from there. Your cloud bill (and CFO) will thank you!

Related essays

Next essay
Vector Databases for AI Applications