Skip to main content
INS // Insights

Serverless Cost Optimization on AWS: Where Bills Hide

Updated June 2026 · 7 min read

Serverless architectures on AWS are sold as cost-efficient by default. They're not. The pay-per-invocation model makes costs invisible until they're large, and the default configurations for Lambda, API Gateway, DynamoDB, and SQS are optimized for convenience, not cost. This is where the bills hide.

Lambda: Memory, Duration, and the Pricing Trap

Lambda pricing is: (number of requests × $0.20/million) + (GB-seconds × $0.0000166667). The GB-seconds component is memory allocation × execution duration. This creates a non-obvious optimization opportunity.

Memory overprovisioning is common. A Lambda function allocated 1024 MB that actually uses 200 MB is paying 5× the memory cost unnecessarily. More surprisingly, because AWS allocates CPU proportionally to memory, higher memory allocations also run faster — often enough that total GB-seconds (and cost) actually decreases.

AWS Lambda Power Tuning (an open-source tool) runs your function at different memory settings and finds the cost-optimal configuration:

# Deploy the Lambda Power Tuning state machine
# https://github.com/alexcasalboni/aws-lambda-power-tuning

# Then invoke it for your function
aws stepfunctions start-execution \
  --state-machine-arn arn:aws:states:us-east-1:ACCOUNT:stateMachine:powerTuningStateMachine \
  --input '{
    "lambdaARN": "arn:aws:lambda:us-east-1:ACCOUNT:function:MyFunction",
    "powerValues": [128, 256, 512, 1024, 2048],
    "num": 50,
    "payload": "{}",
    "parallelInvocation": true,
    "strategy": "cost"
  }'

This is not theoretical — running this analysis typically reveals 20-40% savings from memory right-sizing across a Lambda function fleet.

Cold start mitigation: Lambda cold starts add latency but also cost — Provisioned Concurrency (which keeps instances warm) has its own cost. Whether Provisioned Concurrency reduces total cost depends on your invocation pattern. High-frequency functions (>1 invocation/minute) often don't need it; low-frequency functions with strict latency requirements sometimes do. Compute Savings Plans cover Provisioned Concurrency costs.

ARM (Graviton2) architecture: Lambda supports x86 and arm64 (Graviton2) architectures. Graviton2 Lambda is ~20% cheaper per GB-second and typically faster. Migration requires only a configuration change and a rebuild for the target architecture — zero code changes for Python, Node.js, and most interpreted runtimes.

# In Terraform
resource "aws_lambda_function" "api_handler" {
  architectures = ["arm64"]  # Was ["x86_64"] — 20% cheaper, often faster
  # ... rest of config unchanged
}

API Gateway: Request-Level Costs That Compound

API Gateway charges per API call plus data transfer. At volume, this becomes significant:

  • HTTP API: $1.00/million requests
  • REST API: $3.50/million requests

Use HTTP API, not REST API: If you don't need REST API-specific features (usage plans, API keys, custom authorizers for JWT — HTTP API supports all of these now), HTTP API saves 71% per request. Migrating from REST to HTTP API is the single highest-ROI API Gateway optimization for most teams.

resource "aws_apigatewayv2_api" "main" {
  name          = "main-api"
  protocol_type = "HTTP"  # Not "REST" — much cheaper at scale
}

Response caching: For API endpoints with repetitive read requests, enable caching at the API Gateway level. A cached response doesn't invoke Lambda — saving both Lambda execution cost and API Gateway cost for the request.

Stage-level throttling: Default Lambda concurrency limits apply per account, not per function. Without function-level throttling, a traffic spike to one function can starve other functions of concurrency. Set reserved concurrency on critical functions and maximum concurrency limits on non-critical ones.

DynamoDB: The Expensive Default Settings

DynamoDB is often the largest unexpected cost in serverless architectures.

On-Demand vs. Provisioned Capacity: - On-Demand: you pay per read/write request unit - Provisioned: you pay for allocated capacity whether used or not

On-Demand sounds convenient but is ~6-7× more expensive per request than Provisioned at sustained load. Most production workloads with predictable traffic patterns should use Provisioned with Auto Scaling.

import boto3

dynamodb = boto3.client("dynamodb")

# Switch table to provisioned with auto-scaling
dynamodb.update_table(
    TableName="MyTable",
    BillingMode="PROVISIONED",
    ProvisionedThroughput={
        "ReadCapacityUnits": 10,
        "WriteCapacityUnits": 5,
    }
)

# Set up auto-scaling for the table
autoscaling = boto3.client("application-autoscaling")
autoscaling.register_scalable_target(
    ServiceNamespace="dynamodb",
    ResourceId="table/MyTable",
    ScalableDimension="dynamodb:table:ReadCapacityUnits",
    MinCapacity=5,
    MaxCapacity=100,
)

Table-level vs. item-level cost drivers: - Full table scans cost capacity units proportional to table size — avoid scans in favor of queries with proper partition keys - Large items cost more to read/write — consider storing large payloads in S3 with a reference in DynamoDB - Global Secondary Indexes (GSIs) have their own read/write capacity — monitor GSI costs separately from base table costs

TTL for expiring data: Enable DynamoDB TTL for transient data (sessions, temporary state, cache items). Expired items are deleted automatically without consuming write capacity. Significantly reduces storage costs for time-bounded data.

SQS: The Hidden Cost in Polling

SQS charges per API request — including the polling requests from your consumer. Long polling is both cheaper and more efficient than short polling or frequent polling:

# SHORT POLLING (expensive and wasteful):
while True:
    messages = sqs.receive_message(
        QueueUrl=queue_url,
        MaxNumberOfMessages=10,
        WaitTimeSeconds=0  # Returns immediately even if empty
    )
    # 1 request every few ms = millions of requests/day even when queue is empty
    time.sleep(1)

# LONG POLLING (correct approach):
while True:
    messages = sqs.receive_message(
        QueueUrl=queue_url,
        MaxNumberOfMessages=10,
        WaitTimeSeconds=20  # Waits up to 20s for messages before returning
    )
    # Returns when messages arrive or after 20s max
    # Dramatically fewer requests, lower latency for new messages

Long polling reduces SQS API calls by 90%+ for low-to-medium volume queues. At scale, this is a real cost reduction.

SQS vs. SNS fan-out patterns: If you're using SQS for fan-out (one message going to multiple consumers), SNS + SQS fan-out is cheaper than multiple separate SQS queues all receiving duplicated messages. Each consumer gets its own SQS queue from a single SNS topic — messages are published once.

CloudWatch Logs: The Surprisingly Large Bill

Lambda automatically logs to CloudWatch. At scale, log storage and ingestion becomes a significant cost that most teams don't monitor:

  • CloudWatch Logs ingestion: $0.50/GB
  • Storage: $0.03/GB/month
  • Queries (Insights): $0.005/GB scanned

For a high-volume Lambda fleet, CloudWatch log costs can exceed Lambda execution costs. Mitigation:

# In Lambda function code — be deliberate about what you log
import logging
logger = logging.getLogger()
logger.setLevel(logging.WARNING)  # Not DEBUG or INFO in production

# Structured log only what monitoring needs
logger.info(json.dumps({
    "event": "request_processed",
    "request_id": request_id,
    "duration_ms": elapsed_ms,
    "status": response_status,
}))

Set log group retention periods — default is never-expire. A 30 or 90-day retention covers debugging needs and eliminates permanent accumulation:

resource "aws_cloudwatch_log_group" "lambda_logs" {
  name              = "/aws/lambda/${var.function_name}"
  retention_in_days = 30  # Not 0 (never expire)
}

Rutagon Finds and Fixes Serverless Cost Overruns

If your serverless bill is higher than it should be, Rutagon audits your Lambda, API Gateway, DynamoDB, and SQS configurations and implements the right-sizing changes. Contact us at rutagon.com/contact.

See also: AWS cost monitoring setup and FinOps engineering capabilities.

FAQ

How much can serverless cost optimization realistically save?

In our experience, serverless architectures that haven't been deliberately optimized typically have 30-50% waste from default configurations alone: unoptimized Lambda memory, REST API Gateway where HTTP API would work, DynamoDB on-demand where provisioned makes sense, verbose logging, and missing TTL on temporary data. The savings materialize quickly because most changes are configuration-only with no code changes.

Does Lambda Graviton (arm64) require code changes?

For interpreted runtimes (Python, Node.js, Ruby) and Java: no code changes — just rebuild with the arm64 target. For compiled runtimes (Go, Rust) or functions that depend on native extensions: rebuild for arm64. The architecture change is a Terraform/CloudFormation configuration change. Test in staging first — most workloads see equivalent or better performance at lower cost.

Is DynamoDB On-Demand worth it for any use case?

Yes — for highly unpredictable spiky traffic (e.g., a product that might go viral), On-Demand eliminates the risk of under-provisioned capacity causing throttling. For internal tools and development/staging environments where the extra cost is small, On-Demand is also fine. The key is not using On-Demand as a permanent production setting for workloads with stable, predictable load patterns.

How do you estimate serverless costs before deployment?

AWS has a pricing calculator at calculator.aws.amazon.com. For Lambda, estimate: expected invocations/month, average execution duration, and memory allocation. For DynamoDB, estimate: read/write unit consumption per day. These estimates are rough before you have real traffic data — build monitoring in from day one and review costs weekly during initial scaling.

What's the most common unexpected serverless cost?

Data transfer. Lambda, API Gateway, and DynamoDB all have data transfer costs that are easy to miss in architecture planning. Specifically: data transferred to the internet, cross-AZ data transfer (relevant for multi-AZ DynamoDB Global Tables), and large Lambda response payloads going through API Gateway. Monitor data transfer costs as a separate line item from compute and request costs.

Ready to discuss your project?

We deliver production-grade software for government, defense, and commercial clients. Let's talk about what you need.

Initiate Contact