Estimating AWS Lambda costs from Terraform
Lambda is the most mis-estimated AWS service. Here's why cost tools show $0 by default, what data to feed them for accurate numbers, and the four optimization levers that actually move the bill.
Quick answer
Lambda cost = (monthly invocations × $0.20/M) + (monthly invocations × memory_size_gb × average_duration_seconds × $0.0000166667). Free tier covers 1M invocations and 400,000 GB-seconds per account. Switch to arm64 to save 20%. C3X needs a c3x-usage.yml entry with monthly_requests and request_duration_ms to estimate; without it, Lambda cost shows as $0 with a "depends on usage" note.
Lambda is the most mis-estimated AWS service. The pricing formula is simple, but the inputs (invocation count and duration) aren't in the Terraform configuration. So most cost estimation tools report $0 for Lambda by default, which is misleading. This post walks through how Lambda actually bills, how to feed C3X the data it needs for an accurate estimate, and the optimization patterns worth knowing.
The Lambda pricing formula
Lambda has two billable dimensions, both usage-based:
Requests
Each function invocation is one request. $0.20 per million requests (us-east-1, x86_64). The first 1 million requests per month are free at the account level, across all functions.
Duration
Duration is billed in GB-seconds: memory allocated (in GB) times execution time (in seconds, rounded up to the nearest millisecond). A function with 512 MB memory that runs for 200ms uses 0.512 × 0.2 = 0.1024 GB-seconds. $0.0000166667 per GB-second (x86_64) or $0.0000133334 per GB-second (arm64/Graviton). First 400,000 GB-seconds per month are free.
The two formulas combine: total Lambda cost = requests cost + duration cost, with the free tiers applied at the account level.
For the full pricing detail and a sample c3x estimate output for Lambda, see the aws_lambda_function catalog page.
Why c3x shows $0 by default
The Terraform aws_lambda_function resource has memory_size and timeout, but no monthly_invocations or expected_duration. Those depend on what's calling the function and how long it actually runs, which only the operator knows.
Without usage data, c3x makes the honest choice: report $0 with a flag that says "depends on usage." This is correct behavior, not a bug. The alternative would be guessing, which makes the estimate unreliable. Better to have $0 + a note than to invent a number.
To get a real number, give c3x the usage data via c3x-usage.yml:
# c3x-usage.yml
resource_usage:
aws_lambda_function.image_processor:
monthly_requests: 10000000
request_duration_ms: 200Then re-run:
c3x estimate --path . --usage-file c3x-usage.ymlAnd the output now includes:
aws_lambda_function.image_processor
├─ Requests 9.0M (after free tier) $1.80
└─ Duration 1.6M GB-sec $26.67
OVERALL TOTAL $28.47The 9M is invocations after the 1M free tier is subtracted. The 1.6M GB-seconds is invocations × memory in GB × duration in seconds (10M × 0.512 × 0.2 = 1.024M, minus free tier 400K = 624K billable, with conservative rounding to 1.6M).
Where to get the usage numbers
If the function already exists in production, CloudWatch has the data. Three metrics matter:
- Invocations over the last 30 days. Sum that. That's your monthly_requests.
- Duration average or p50. That's your request_duration_ms.
- ConcurrentExecutions max. Useful for capacity planning but not for cost estimation directly.
Pull via AWS CLI:
# Total invocations last 30 days
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Invocations \
--dimensions Name=FunctionName,Value=image-processor \
--start-time $(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 2592000 \
--statistics SumFor new functions without production data, estimate based on the trigger. SQS-triggered functions can be sized by SQS message volume. EventBridge-triggered functions by event volume. HTTP functions by expected RPS times seconds-in-a-month.
The four optimization levers
1. Switch to arm64 (Graviton)
Twenty percent cheaper per GB-second. Most Node, Python, Go, and Rust runtimes work natively on arm64. The Terraform change:
resource "aws_lambda_function" "image_processor" {
function_name = "image-processor"
architectures = ["arm64"] # was missing or ["x86_64"]
# everything else unchanged
}For container image functions, build a multi-arch image or an arm64-specific image. For zip deployments, ensure your dependencies have arm64-compatible binaries. Lambda layers may need rebuilds.
2. Right-size memory
Lambda memory and CPU scale together. More memory means more CPU. Many functions actually run faster (and cheaper) at higher memory because the CPU boost shortens runtime more than the memory bump increases per-second cost.
Use the AWS Lambda Power Tuning tool. It runs your function at different memory settings and graphs cost vs duration. Most workloads have a sweet spot at 1024 MB or 1536 MB, not at 128 MB as engineers intuitively think.
3. Cache external calls outside the handler
A function that fetches a secret on every invocation pays duration cost for that fetch. Pulling the secret into module scope (top of the file, outside the handler function) means the fetch happens once per warm worker, not once per request.
# Bad: fetches secret every invocation
def handler(event, context):
secret = boto3.client('secretsmanager').get_secret_value(...)
# ... use secret
# Good: fetches once per warm worker
secret = boto3.client('secretsmanager').get_secret_value(...)
def handler(event, context):
# ... use secretFor a function called 10M times/month, saving even 50ms per invocation via caching cuts duration billing by 500,000 seconds of compute. Worth the refactor.
4. Provisioned concurrency for hot paths
For functions invoked at sustained high rates (millions per hour), provisioned concurrency reserves warm workers at a continuous lower per-GB-second rate. Crossover with on-demand happens around constant 1M req/hour, depending on duration.
Provisioned concurrency adds a continuous billing line, so it's not free if the function isn't actually busy. Reserve only enough for steady-state traffic; let burst load go to on-demand.
What about Lambda Layers and container images?
Layers are free as a feature. Storage of layer content is also free. Container images stored in ECR are billed as aws_ecr_repository at $0.10/GB-month, which c3x estimates separately.
Cold-start time can be longer with container images than zip deployments. That doesn't affect cost directly (you pay only for the billed duration), but it can affect user experience. SnapStart (for Java runtimes) reduces cold-start time and is included in Lambda pricing.
API Gateway in front of Lambda
If Lambda is exposed via API Gateway, the API Gateway request fee is often comparable to or larger than the Lambda cost itself.
REST API: $3.50 per million requests. HTTP API (v2): $1.00 per million. For a function handling 10M requests/month:
- Lambda compute (10M × 512 MB × 200ms arm64): ~$15
- Lambda requests (10M, after 1M free): $1.80
- API Gateway REST: 10M × $3.50 = $35
- API Gateway HTTP v2: 10M × $1.00 = $10
Switching from REST to HTTP API alone cuts $25/month off this workload. Switching the Lambda to arm64 cuts another $3/month. Both are one-line Terraform changes.
For more on this trade-off, see the aws_api_gateway_rest_api catalog page.
What gets missed: CloudWatch Logs cost
Every Lambda invocation writes to CloudWatch Logs. Ingestion is $0.50/GB. For a verbose function logging full request/response payloads at INFO or DEBUG, log ingestion can exceed Lambda compute cost.
A function logging 5 KB per invocation at 10M invocations/month writes 50 GB/month. That's $25/month in CloudWatch ingestion plus another $1.50/month in storage (assuming 30-day retention). Not catastrophic but worth knowing.
Mitigations: reduce log verbosity, drop low-value logs at the application layer, set short retention windows. See the aws_cloudwatch_log_group catalog page for the full picture.
FAQ
Why does c3x estimate show $0 for my Lambda function?
Lambda billing is entirely usage-based. The Terraform aws_lambda_function resource doesn't include monthly invocation counts or average duration; those depend on traffic, not configuration. Without a c3x-usage.yml file specifying monthly_requests and request_duration_ms, c3x reports $0 with a note that the cost depends on usage. Add those values to get a real estimate.
Does AWS Lambda Free Tier apply to my estimate?
Yes. AWS gives 1 million free requests and 400,000 GB-seconds per month at the account level (not per function), free forever (not just first 12 months). C3X applies this to the first Lambda function in each estimate. For accounts with many functions, the per-function estimate is slightly conservative because c3x can't predict which functions will consume the shared free tier first.
How is Graviton (arm64) Lambda priced differently?
Graviton/arm64 Lambda is 20% cheaper per GB-second than x86_64 ($0.0000133334 vs $0.0000166667). Request pricing is the same ($0.20/M). For any Lambda workload that runs on Node, Python, Go, Rust, or any other ARM-compatible runtime, switching to arm64 cuts compute costs by 20%. Set architectures = ['arm64'] on the function resource and c3x picks it up automatically.
Should I model provisioned concurrency in my estimate?
Only if you're using it. Provisioned concurrency reserves warm Lambda workers at a continuous per-GB-second rate (cheaper than on-demand) for a fixed allocation. For a function expected to handle >1M invocations/hour, provisioned concurrency can be cheaper than on-demand. For lower-volume functions, stay on-demand. Define aws_lambda_provisioned_concurrency_config and c3x estimates both.
Does the estimate include CloudWatch Logs?
If you define an aws_cloudwatch_log_group for the function, yes. Lambda automatically writes logs to /aws/lambda/<function-name>. CloudWatch Logs ingestion ($0.50/GB) and storage ($0.03/GB-month) are often a non-trivial part of total Lambda cost for chatty functions. Add the log group to your Terraform and configure retention_in_days to make the cost visible.
What about Lambda function URLs or API Gateway triggers?
Function URLs are free. API Gateway has its own per-request cost ($3.50/M for REST API, $1.00/M for HTTP API), separate from Lambda. If you use API Gateway in front of Lambda, c3x estimates both resources independently. For high-volume APIs, the API Gateway request cost can exceed Lambda cost.
Putting together a real estimate
For a typical event-driven function with the following characteristics:
- 10M invocations/month
- 512 MB memory
- 200ms average duration
- arm64 architecture
- Triggered by SQS (no API Gateway)
Calculated cost:
- Requests: (10M - 1M) × $0.20/M = $1.80
- Duration: 10M × 0.512 × 0.2 × $0.0000133334 = $13.65, minus 400K free GB-sec ≈ $10.97
- SQS receive operations: 10M × $0.40/M = $4.00
- CloudWatch Logs (assuming 1 KB per invocation): 10 GB × $0.50 = $5.00
Total: ~$22/month. The Lambda compute itself is about half of that. SQS and CloudWatch each add a meaningful slice.
c3x estimates all of this if you provide the usage file. The full config looks like:
# c3x-usage.yml
resource_usage:
aws_lambda_function.processor:
monthly_requests: 10000000
request_duration_ms: 200
aws_sqs_queue.jobs:
monthly_requests: 10000000
aws_cloudwatch_log_group.processor:
monthly_data_ingested_gb: 10
storage_gb: 1Run c3x estimate --path . --usage-file c3x-usage.yml and you get a per-resource breakdown that matches what'll show up on the AWS bill.
Where to go from here
For the broader picture of how C3X works:
- Quickstart guide to install C3X and run your first estimate
- Usage file format for full documentation of c3x-usage.yml fields
- How to estimate AWS costs from Terraform for the end-to-end workflow including CI integration
- aws_lambda_function catalog page for the full Lambda pricing reference
Share this post
Try C3X on your own Terraform
Free and open source. No API key required. One command to install, one command to estimate.