API Gateway without caching means every request hits your backendโeven for identical data. Enable caching with Terraform and cut Lambda invocations by 90%.
Question: How many times do your users request the same data?
Answer: Way more than you think.
Every API call without caching triggers:
- Lambda invocation
- Database query
- API response serialization
- All the compute costs
Even when the data hasnโt changed.
Hereโs a typical high-traffic API:
Requests: 10 million/month
Cacheable: 80% (product info, configs, public data)
Cache hit rate: 90%
Without caching:
- 10M Lambda invocations
- Cost: $200/month
With caching:
- 1M Lambda invocations (9M served from cache)
- Cost: $20/month + $28 (cache)
- Total: $48/month
Savings: $152/month = $1,824/year (76% reduction!) ๐ฐ
Let me show you how to enable API Gateway caching with Terraform.
๐ธ The Cost of Not Caching
API Gateway pricing:
- Requests: $3.50 per million
- Lambda: $0.20 per 1M requests + duration
- Cache: $0.02/hour per GB (flat fee)
Example: Public API serving product catalog
Traffic: 5M requests/month
Cacheable data: 90%
Average Lambda duration: 100ms
Lambda memory: 512MB
Without cache:
- 5M Lambda invocations
- 5M ร 0.1s ร $0.0000083/GB-s = $41.50
- API Gateway: 5M ร $0.0000035 = $17.50
- Total: $59/month
With cache (0.5GB, 1 hour TTL):
- Cache cost: 0.5GB ร $0.02/hr ร 730hrs = $7.30/month
- Lambda (10% requests): $4.15
- API Gateway: $17.50
- Total: $28.95/month
Savings: $30/month = $360/year (51% reduction!)
๐ฏ When to Use Caching
Perfect for:
- โ Product catalogs
- โ Configuration endpoints
- โ Public reference data
- โ Weather/stock prices (stale for 1-5 min is fine)
- โ User profiles (change infrequently)
- โ Search results
- โ Rate-limited external APIs
Donโt cache:
- โ User-specific authenticated data (unless per-user cache)
- โ Real-time data requiring sub-second freshness
- โ POST/PUT/DELETE requests
- โ Payment processing
๐ ๏ธ Terraform Implementation
Basic REST API with Caching
# api-gateway-cache.tf
resource "aws_api_gateway_rest_api" "api" {
name = "products-api"
}
resource "aws_api_gateway_resource" "products" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = "products"
}
resource "aws_api_gateway_method" "get_products" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.products.id
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "lambda" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.products.id
http_method = aws_api_gateway_method.get_products.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.products.invoke_arn
# Enable caching for this integration
cache_key_parameters = []
cache_namespace = "products"
}
# Enable caching at the stage level
resource "aws_api_gateway_deployment" "api" {
rest_api_id = aws_api_gateway_rest_api.api.id
depends_on = [
aws_api_gateway_integration.lambda
]
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_stage" "prod" {
deployment_id = aws_api_gateway_deployment.api.id
rest_api_id = aws_api_gateway_rest_api.api.id
stage_name = "prod"
# Enable caching!
cache_cluster_enabled = true
cache_cluster_size = "0.5" # 0.5GB cache
# Cache settings
variables = {
cacheEnabled = "true"
}
}
resource "aws_api_gateway_method_settings" "products_cache" {
rest_api_id = aws_api_gateway_rest_api.api.id
stage_name = aws_api_gateway_stage.prod.stage_name
method_path = "${aws_api_gateway_resource.products.path_part}/${aws_api_gateway_method.get_products.http_method}"
settings {
caching_enabled = true
cache_ttl_in_seconds = 300 # 5 minutes
cache_data_encrypted = true
# Optional: Require cache key
require_authorization_for_cache_control = true
}
}
HTTP API with Caching (Simpler, Cheaper)
# Note: HTTP APIs don't support built-in caching
# Use CloudFront instead for caching HTTP APIs
resource "aws_cloudfront_distribution" "api_cache" {
enabled = true
origin {
domain_name = "${aws_apigatewayv2_api.http_api.id}.execute-api.${var.region}.amazonaws.com"
origin_id = "api-gateway"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "api-gateway"
viewer_protocol_policy = "redirect-to-https"
cache_policy_id = aws_cloudfront_cache_policy.api.id
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
resource "aws_cloudfront_cache_policy" "api" {
name = "api-cache-policy"
default_ttl = 300
max_ttl = 3600
min_ttl = 60
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "none"
}
headers_config {
header_behavior = "whitelist"
headers {
items = ["Accept", "Authorization"]
}
}
query_strings_config {
query_string_behavior = "all"
}
}
}
Production Module with Per-Endpoint Configuration
# modules/cached-api/main.tf
variable "api_name" {
description = "API name"
type = string
}
variable "cache_size" {
description = "Cache size (0.5, 1.6, 6.1, 13.5, 28.4, 58.2, 118, 237)"
type = string
default = "0.5"
}
variable "endpoints" {
description = "API endpoints with cache configuration"
type = map(object({
path = string
method = string
lambda_arn = string
cache_ttl = number
cache_enabled = bool
}))
}
resource "aws_api_gateway_rest_api" "api" {
name = var.api_name
}
resource "aws_api_gateway_resource" "endpoints" {
for_each = var.endpoints
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = each.value.path
}
resource "aws_api_gateway_method" "methods" {
for_each = var.endpoints
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.endpoints[each.key].id
http_method = each.value.method
authorization = "NONE"
}
resource "aws_api_gateway_integration" "integrations" {
for_each = var.endpoints
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.endpoints[each.key].id
http_method = aws_api_gateway_method.methods[each.key].http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = each.value.lambda_arn
cache_namespace = each.key
}
resource "aws_api_gateway_deployment" "api" {
rest_api_id = aws_api_gateway_rest_api.api.id
depends_on = [
aws_api_gateway_integration.integrations
]
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_stage" "prod" {
deployment_id = aws_api_gateway_deployment.api.id
rest_api_id = aws_api_gateway_rest_api.api.id
stage_name = "prod"
cache_cluster_enabled = true
cache_cluster_size = var.cache_size
}
resource "aws_api_gateway_method_settings" "cache_settings" {
for_each = {
for k, v in var.endpoints : k => v if v.cache_enabled
}
rest_api_id = aws_api_gateway_rest_api.api.id
stage_name = aws_api_gateway_stage.prod.stage_name
method_path = "${each.value.path}/${each.value.method}"
settings {
caching_enabled = true
cache_ttl_in_seconds = each.value.cache_ttl
cache_data_encrypted = true
}
}
output "api_endpoint" {
value = aws_api_gateway_stage.prod.invoke_url
}
Usage
module "products_api" {
source = "./modules/cached-api"
api_name = "products-api"
cache_size = "0.5" # 0.5GB cache
endpoints = {
products = {
path = "products"
method = "GET"
lambda_arn = aws_lambda_function.get_products.invoke_arn
cache_ttl = 300 # 5 minutes
cache_enabled = true
}
categories = {
path = "categories"
method = "GET"
lambda_arn = aws_lambda_function.get_categories.invoke_arn
cache_ttl = 600 # 10 minutes
cache_enabled = true
}
orders = {
path = "orders"
method = "POST"
lambda_arn = aws_lambda_function.create_order.invoke_arn
cache_ttl = 0
cache_enabled = false # Don't cache POST
}
}
}
๐ก Pro Tips
1. Choose the Right Cache Size
# Cache sizes and costs
cache_cluster_size = "0.5" # $0.02/hr = $14.60/month
cache_cluster_size = "1.6" # $0.038/hr = $27.74/month
cache_cluster_size = "6.1" # $0.20/hr = $146/month
cache_cluster_size = "13.5" # $0.25/hr = $182.50/month
Start with 0.5GB. Monitor CacheHitCount and CacheMissCount metrics.
2. Set Appropriate TTL
# Based on data freshness requirements
cache_ttl_in_seconds = 60 # 1 minute (near real-time)
cache_ttl_in_seconds = 300 # 5 minutes (common)
cache_ttl_in_seconds = 3600 # 1 hour (static data)
cache_ttl_in_seconds = 86400 # 24 hours (rarely changes)
3. Use Cache Keys for Variations
resource "aws_api_gateway_integration" "lambda" {
# Cache by query parameters
cache_key_parameters = ["method.request.querystring.category"]
# Each category gets its own cache entry
}
4. Monitor Cache Performance
resource "aws_cloudwatch_dashboard" "api_cache" {
dashboard_name = "api-gateway-cache"
dashboard_body = jsonencode({
widgets = [
{
type = "metric"
properties = {
metrics = [
["AWS/ApiGateway", "CacheHitCount", { stat = "Sum" }],
[".", "CacheMissCount", { stat = "Sum" }]
]
period = 300
region = var.region
title = "Cache Hit vs Miss"
}
}
]
})
}
# Alert on low cache hit rate
resource "aws_cloudwatch_metric_alarm" "low_cache_hit" {
alarm_name = "api-low-cache-hit-rate"
comparison_operator = "LessThanThreshold"
evaluation_periods = 2
threshold = 70 # Alert if < 70% hit rate
metric_query {
id = "hit_rate"
expression = "(hits / (hits + misses)) * 100"
label = "Cache Hit Rate %"
return_data = true
}
metric_query {
id = "hits"
metric {
metric_name = "CacheHitCount"
namespace = "AWS/ApiGateway"
period = 300
stat = "Sum"
}
}
metric_query {
id = "misses"
metric {
metric_name = "CacheMissCount"
namespace = "AWS/ApiGateway"
period = 300
stat = "Sum"
}
}
}
5. Invalidate Cache When Needed
# In Lambda that updates data
import boto3
api_gateway = boto3.client('apigateway')
def handler(event, context):
# Update data...
# Invalidate cache
api_gateway.flush_stage_cache(
restApiId='abc123',
stageName='prod'
)
๐ Real-World Example
E-commerce API with product catalog
Before caching:
Traffic: 20M requests/month
- 18M product lookups
- 2M order creation
Lambda costs:
- 20M invocations ร $0.20/1M = $4
- Duration: 100ms ร 20M ร $0.0000083 = $166
Total: $170/month
After caching (5-min TTL, 90% hit rate):
Cache cost: 0.5GB ร $0.02/hr ร 730hrs = $14.60/month
Lambda costs:
- Product lookups: 1.8M invocations (90% cached)
- Orders: 2M invocations (not cached)
- Total: 3.8M invocations
- Cost: 3.8M ร 100ms ร $0.0000083 = $31.50
Total: $14.60 + $31.50 = $46.10/month
Savings: $123.90/month = $1,487/year (73% reduction!) ๐
๐ Quick Start
# 1. Enable caching on existing API
terraform apply
# 2. Test cache is working
curl -I https://api.example.com/products
# Response headers show cache status:
# X-Cache: Hit from cloudfront
# X-Amz-Cf-Pop: IAD89-C1
# 3. Monitor cache hit rate
aws cloudwatch get-metric-statistics \
--namespace AWS/ApiGateway \
--metric-name CacheHitCount \
--dimensions Name=ApiName,Value=products-api \
--start-time 2024-01-01T00:00:00Z \
--end-time 2024-01-31T23:59:59Z \
--period 86400 \
--statistics Sum
# 4. Watch Lambda invocations drop ๐ฐ
โ ๏ธ Common Gotchas
1. HTTP APIs Donโt Support Native Caching
Use CloudFront instead:
# REST API: Native caching โ
cache_cluster_enabled = true
# HTTP API: Use CloudFront โ
resource "aws_cloudfront_distribution" "api_cache" { ... }
2. Cache Keys Must Include Variations
# WRONG - All users get same cached response
cache_key_parameters = []
# RIGHT - Cache per user
cache_key_parameters = ["method.request.header.Authorization"]
3. Default Cache Size May Be Small
# Too small for high traffic
cache_cluster_size = "0.5" # Only 0.5GB
# Monitor and increase if needed
cache_cluster_size = "1.6" # 1.6GB
๐ฏ Summary
The Problem:
- Every API call hits Lambda (even for identical data)
- High Lambda costs for cacheable endpoints
- Unnecessary database queries
The Solution:
- Enable API Gateway caching
- Set appropriate TTL (5-60 minutes)
- Monitor cache hit rate
The Result:
- Typical savings: 70-90% on Lambda costs
- Faster response times (cache < Lambda)
- Reduced backend load
Implementation:
- Add
cache_cluster_enabled = true - Set cache size (start with 0.5GB)
- Configure per-endpoint TTL
- Cost: $15-30/month for most APIs
Stop invoking Lambda for data that hasnโt changed. Enable caching and cut your API costs by 70%+. ๐
Enabled API Gateway caching? Whatโs your cache hit rate? Share in the comments! ๐ฌ
Follow for more AWS cost optimization with Terraform! โก
Top comments (0)