CloudFront Pricing Breakdown
CloudFront is dramatically cheaper than serving traffic directly from EC2 or S3. But "cheap" does not mean "free", and CloudFront bills accumulate through several mechanisms that are easy to optimize.
CloudFront Cost Components:
1. Data Transfer Out (to internet):
PriceClass_100 (US/Europe/Israel):
First 10TB: $0.0085/GB
Next 40TB: $0.0080/GB
Next 100TB: $0.0060/GB
PriceClass_200 (+ Asia Pacific, Middle East, Africa):
First 10TB: $0.0120/GB (some regions up to $0.0170/GB)
PriceClass_All (all regions):
Most expensive — South America: $0.025/GB, India: $0.017/GB
2. HTTP/HTTPS Requests:
$0.0100 per 10,000 HTTPS requests
$0.0075 per 10,000 HTTP requests
3. Origin Fetch (CloudFront to your origin):
EC2/ALB: $0.020/GB
S3 in same region: FREE
4. Lambda@Edge:
$0.60 per 1M requests + $0.00005001 per GB-second
Key insight: A 1MB image served to 1M users/month:
Direct S3 egress: 1TB Ă— $0.09 = $92.16
Via CloudFront: 1TB Ă— $0.0085 = $8.70 (91% cheaper!)
Origin fetch (S3): FREE (same region)
Price Class Selection
resource "aws_cloudfront_distribution" "main" {
# WRONG: Using PriceClass_All for a US-only business
# price_class = "PriceClass_All" # Expensive, Australia/Brazil/India edge nodes
# RIGHT: Match price class to actual user geography
price_class = "PriceClass_100" # US, Canada, Europe, Israel only
# Saves: 30-40% on transfer costs vs PriceClass_All
# For global audience:
# price_class = "PriceClass_200" # Adds Asia Pacific + Middle East
# price_class = "PriceClass_All" # Only if South America/Africa matter
# How to decide:
# 1. Check CloudFront Access Logs for viewer country distribution
# 2. If 95% of traffic is US/Europe: PriceClass_100
# 3. If significant Asia/Pacific: PriceClass_200
# 4. PriceClass_All only if global enterprise with regional SLAs
}
Cache Hit Rate Optimization
The single most important CloudFront metric: Cache Hit Rate
Goal: 90%+ cache hit rate
Low cache hit rate means:
- More origin fetches (costs $0.020/GB instead of serving from edge)
- Higher latency for users
- More load on your origin servers
What kills your cache hit rate:
1. Query strings creating unique cache keys unnecessarily
/api/products?sort=price&page=1 -- different cache key than
/api/products?page=1&sort=price -- same content, different order!
2. Cookies being forwarded to origin (disables caching for that path)
session_id cookie = unique per user = 0% cache hit rate
3. Authorization headers vary the cache
Each user's token = unique cache entry = effectively no caching
4. Volatile URL parameters (tracking: ?utm_source=google)
utm_source, fbclid, gclid should be stripped or ignored in cache key
# Managed Cache Policies (use these instead of custom)
# CachingOptimized: best for S3 static assets
# CachingDisabled: for API endpoints that must not be cached
# CachingOptimizedForUncompressedObjects: when compression is handled elsewhere
# Custom cache policy for static assets with query string handling
resource "aws_cloudfront_cache_policy" "static_assets" {
name = "static-assets-policy"
min_ttl = 86400 # 1 day minimum
default_ttl = 604800 # 7 days default
max_ttl = 31536000 # 1 year maximum
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "none" # Don't include cookies in cache key
}
headers_config {
header_behavior = "none" # Don't include headers in cache key
}
query_strings_config {
query_string_behavior = "whitelist"
query_strings {
items = ["v", "version"] # Only cache-bust version param
}
}
enable_accept_encoding_gzip = true # CRITICAL: enables compression
enable_accept_encoding_brotli = true # Brotli = smaller than gzip
}
}
# Cache policy for authenticated API endpoints (must not cache)
resource "aws_cloudfront_cache_policy" "api_nocache" {
name = "api-nocache"
min_ttl = 0
default_ttl = 0
max_ttl = 0
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config { cookie_behavior = "none" }
headers_config { header_behavior = "none" }
query_strings_config { query_string_behavior = "none" }
}
}
# Origin Request Policy — what to forward to origin (separate from cache key)
resource "aws_cloudfront_origin_request_policy" "api" {
name = "api-origin-request"
cookies_config {
cookie_behavior = "all" # Forward all cookies to origin (but don't cache on them)
}
headers_config {
header_behavior = "whitelist"
headers {
items = ["Authorization", "Accept", "Content-Type"]
}
}
query_strings_config {
query_string_behavior = "all" # Forward all QS to origin
}
}
Compression: Reduce Transfer Costs 60-80%
CloudFront supports automatic compression for text-based content:
HTML, CSS, JavaScript, JSON, XML, SVG, fonts
Without compression:
1MB JavaScript file Ă— 10M requests Ă— $0.0085/GB = $85/month
With gzip compression (typical 70% reduction):
300KB Ă— 10M requests Ă— $0.0085/GB = $25.50/month
Saving: $59.50/month per asset per 10M requests
With Brotli (typical 80% reduction vs uncompressed, 15% better than gzip):
200KB Ă— 10M requests Ă— $0.0085/GB = $17/month
Saving: $68/month per asset
Enabling compression in CloudFront:
1. Set compress = true in cache behavior (Terraform)
2. Ensure origin does NOT double-compress (CloudFront compresses edge-side)
3. Set enable_accept_encoding_gzip = true in cache policy
4. Set enable_accept_encoding_brotli = true for Brotli support
Pre-compress at build time + serve with Content-Encoding header:
Even better: pre-gzip/brotli assets, upload to S3 with Content-Encoding header
CloudFront serves pre-compressed file directly — no CPU overhead
Caching Headers from Your Origin
# FastAPI: Set cache headers to control CloudFront TTL
from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/api/products")
async def get_products(response: Response):
products = await fetch_products()
# Tell CloudFront: cache this for 5 minutes
response.headers["Cache-Control"] = "public, max-age=300, s-maxage=300"
response.headers["Vary"] = "Accept-Encoding" # Vary on compression
return products
@app.get("/api/products/{id}/inventory")
async def get_inventory(id: int, response: Response):
# Real-time data — do not cache
response.headers["Cache-Control"] = "no-cache, no-store"
return await fetch_inventory(id)
@app.get("/static/config.json")
async def get_config(response: Response):
# Immutable config — cache for 7 days
response.headers["Cache-Control"] = "public, max-age=604800, immutable"
return await get_app_config()
# Cache-Control reference:
# public, max-age=N = CDN and browser cache for N seconds
# s-maxage=N = CDN caches for N seconds (overrides max-age for CDN)
# private, max-age=N = Browser only, CDN does NOT cache
# no-cache = Revalidate on every request
# no-store = Never cache anywhere
# immutable = Never re-fetch (use only with content-hashed URLs)
# stale-while-revalidate = Serve stale while fetching fresh in background
CloudFront Access Logs for Cost Analysis
# Enable standard logging (costs $0.0009/10K requests in S3)
# Use for periodic analysis, not always-on
# Analyze cache hit rate from logs using Athena
# CREATE EXTERNAL TABLE cloudfront_logs (
# date DATE, time STRING, location STRING, bytes BIGINT,
# ip STRING, method STRING, host STRING, uri STRING,
# status INT, referer STRING, useragent STRING,
# uriquery STRING, cookie STRING, resulttype STRING, ...
# )
# PARTITIONED BY (year STRING, month STRING, day STRING)
# ROW FORMAT DELIMITED FIELDS TERMINATED BY ' '
# LOCATION 's3://my-cf-logs/';
# Cache hit rate query:
# SELECT
# resulttype,
# COUNT(*) as requests,
# ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER(), 2) as pct
# FROM cloudfront_logs
# WHERE year='2026' AND month='03'
# GROUP BY resulttype;
# resulttype values:
# Hit = served from edge cache (good!)
# Miss = origin fetch required (costly)
# RefreshHit = conditional GET revalidation
# LambdaGeneratedResponse = Lambda@Edge response
# Target: Hit% > 90%, Miss% < 10%
Real-World Optimization Results
Before optimization (50TB/month traffic):
PriceClass_All: $2,500/month transfer
No compression: $2,500/month (uncompressed assets)
Cache hit rate: 55% Many unnecessary origin fetches
Origin fetch: $650/month (30TB to origin)
Total CloudFront: $5,650/month
After optimization:
PriceClass_100 (US/EU): $850/month (traffic profile was 95% US/EU)
Brotli compression 80%: $510/month transfer
Cache hit rate: 94%: Origin fetch reduced by 70%
Origin fetch (S3 free): $0 (moved origin to S3 same region)
Total CloudFront: $1,360/month
Savings: $4,290/month (76% reduction)
Key changes:
1. Price class change: saved $1,650/month instantly
2. Compression: saved $1,990/month
3. Cache hit rate fix: saved $455/month in origin fetch
4. Origin to S3 (free origin fetch): saved $650/month
Conclusion
CloudFront cost optimization is dominated by three levers: the right price class for your audience geography, maximum compression for text assets, and a high cache hit rate through sensible cache policies. Each lever independently delivers 20-40% savings, and together they can reduce CloudFront costs by 60-80% compared to a default configuration.
Priya Sharma
Full-Stack Developer and open-source contributor with a passion for performance and developer experience.