DEV Community

Cover image for Your API Calls the Same Lambda 1,000 Times a Minute (Enable Caching Already) ๐Ÿš€
Suhas Mallesh
Suhas Mallesh

Posted on • Edited on

Your API Calls the Same Lambda 1,000 Times a Minute (Enable Caching Already) ๐Ÿš€

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!) ๐Ÿ’ฐ
Enter fullscreen mode Exit fullscreen mode

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!)
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฏ 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
    )
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Š 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
Enter fullscreen mode Exit fullscreen mode

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!) ๐ŸŽ‰
Enter fullscreen mode Exit fullscreen mode

๐Ÿš€ 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 ๐Ÿ’ฐ
Enter fullscreen mode Exit fullscreen mode

โš ๏ธ 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" { ... }
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฏ 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)