Tarek Cheikh
Founder & AWS Cloud Architect
CloudFront is the AWS content delivery network. It caches content at edge locations worldwide so users receive responses from the nearest location instead of the origin server. This reduces latency, offloads traffic from the origin, and includes DDoS protection through AWS Shield Standard at no extra cost.
This article covers CloudFront from distribution creation to production patterns: S3 origins with Origin Access Control, cache policies, path-based routing, origin failover, CloudFront Functions, Lambda@Edge, WAF integration, signed URLs, cache invalidation, pricing, and monitoring.
# CloudFront request flow:
#
# 1. User requests https://d111111abcdef8.cloudfront.net/image.jpg
# 2. DNS resolves to the nearest CloudFront edge location
# 3. Edge checks its local cache
# - HIT: return cached object immediately
# - MISS: forward to regional edge cache
# 4. Regional edge cache checks its cache
# - HIT: return to edge, cache locally, serve to user
# - MISS: fetch from origin (S3, ALB, or custom HTTP server)
# 5. Response cached at both regional edge cache and edge location
# 6. Subsequent requests from nearby users hit the edge cache
#
# Infrastructure (2025):
# - 600+ Points of Presence (PoPs) in 100+ cities, 50+ countries
# - 13 regional edge caches (larger caches, longer retention)
# - AWS global backbone network between edge and origin (not public internet)
#
# Two-tier caching:
#
# [User] --> [Edge Location] --> [Regional Edge Cache] --> [Origin]
# 450+ locations 13 locations S3 / ALB / HTTP
# local cache shared cache your server
Origin Access Control (OAC) is the recommended method for S3 origins. It replaces the legacy Origin Access Identity (OAI) and supports SSE-KMS encrypted objects, all HTTP methods (including PUT and DELETE), and S3 Object Lambda.
# Step 1: Create an Origin Access Control
aws cloudfront create-origin-access-control \
--origin-access-control-config '{
"Name": "my-site-oac",
"Description": "OAC for S3 static website",
"SigningProtocol": "sigv4",
"SigningBehavior": "always",
"OriginAccessControlOriginType": "s3"
}'
# Note the Id from the response: OriginAccessControl.Id (e.g., E2QWRUHAPOMQZL)
# Step 2: Create the distribution
# Save the JSON below as distribution.json
aws cloudfront create-distribution \
--distribution-config file://distribution.json
{
"CallerReference": "my-site-2025-01",
"Comment": "Static website with OAC",
"Enabled": true,
"DefaultRootObject": "index.html",
"HttpVersion": "http2and3",
"PriceClass": "PriceClass_All",
"Origins": {
"Quantity": 1,
"Items": [
{
"Id": "s3-origin",
"DomainName": "my-site-bucket.s3.us-east-1.amazonaws.com",
"OriginAccessControlId": "E2QWRUHAPOMQZL",
"S3OriginConfig": {
"OriginAccessIdentity": ""
}
}
]
},
"DefaultCacheBehavior": {
"TargetOriginId": "s3-origin",
"ViewerProtocolPolicy": "redirect-to-https",
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
"Compress": true,
"AllowedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"],
"CachedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"]
}
}
},
"ViewerCertificate": {
"CloudFrontDefaultCertificate": true
}
}
# CachePolicyId "658327ea-..." is the AWS managed CachingOptimized policy:
# MinTTL: 1s, DefaultTTL: 86400s (24h), MaxTTL: 31536000s (365 days)
# Gzip and Brotli compression enabled in the cache key
#
# S3OriginConfig with empty OriginAccessIdentity is required even with OAC.
# This is an API requirement -- do not remove it.
#
# HttpVersion "http2and3" enables HTTP/2 and HTTP/3 (QUIC).
# HTTP/3 reduces connection setup time, especially on mobile networks.
# Step 3: Grant CloudFront access to the S3 bucket
# Save as bucket-policy.json, replace the distribution ARN with yours
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOAC",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-site-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E1A2B3C4D5E6F7"
}
}
}
]
}
aws s3api put-bucket-policy \
--bucket my-site-bucket \
--policy file://bucket-policy.json
# The Condition block restricts access to YOUR specific distribution.
# Without it, any CloudFront distribution could read your objects.
# Verify the distribution is deployed (takes 5-10 minutes)
aws cloudfront get-distribution --id E1A2B3C4D5E6F7 \
--query 'Distribution.Status'
# "Deployed"
# CloudFront requires ACM certificates in us-east-1 regardless of
# where your origin is located. CloudFront is a global service and
# uses the us-east-1 region for certificate management.
# Step 1: Request a certificate in us-east-1
aws acm request-certificate \
--domain-name cdn.example.com \
--subject-alternative-names "*.example.com" \
--validation-method DNS \
--region us-east-1
# Step 2: Complete DNS validation (add the CNAME record ACM provides)
aws acm describe-certificate \
--certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/abc-123 \
--region us-east-1 \
--query 'Certificate.DomainValidationOptions[0].ResourceRecord'
# Step 3: Update the distribution with the custom domain
# Get the current config and ETag
aws cloudfront get-distribution-config --id E1A2B3C4D5E6F7 > dist-config.json
# Edit dist-config.json:
# - Replace ViewerCertificate with:
# "ViewerCertificate": {
# "ACMCertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/abc-123",
# "SSLSupportMethod": "sni-only",
# "MinimumProtocolVersion": "TLSv1.2_2021"
# }
# - Add Aliases:
# "Aliases": {"Quantity": 1, "Items": ["cdn.example.com"]}
# Apply the update (use the ETag from the get-distribution-config response)
aws cloudfront update-distribution \
--id E1A2B3C4D5E6F7 \
--if-match E2QWRUHEXAMPLE \
--distribution-config file://dist-config-updated.json
# SSLSupportMethod "sni-only" uses Server Name Indication (free).
# The alternative "vip" dedicates an IP address ($600/month).
# MinimumProtocolVersion "TLSv1.2_2021" enforces TLS 1.2 with modern ciphers.
# Step 4: Create a DNS CNAME record
# cdn.example.com CNAME d111111abcdef8.cloudfront.net
# Cache policies control what goes into the cache key and how long
# objects are cached. AWS provides managed policies for common patterns:
#
# Managed Cache Policies:
# Name ID Use Case
# -------------------------------------------------------------------------------------------------
# CachingOptimized 658327ea-f89d-4fab-a63d-7e88639e58f6 S3, static assets
# CachingDisabled 4135ea2d-6df8-44a3-9df3-4b5a84be39ad APIs, dynamic content
# CachingOptimizedForUncompressed b2884449-e4de-46a7-ac36-70bc7f1ddd6d Pre-compressed content
#
# List all managed policies:
aws cloudfront list-cache-policies --type managed \
--query 'CachePolicyList.Items[].CachePolicy.{Name:CachePolicyConfig.Name,Id:Id}'
# Create a custom cache policy (cache API responses by query string)
aws cloudfront create-cache-policy --cache-policy-config '{
"Name": "CustomAPI",
"Comment": "Cache API responses by path and query strings",
"DefaultTTL": 300,
"MaxTTL": 3600,
"MinTTL": 0,
"ParametersInCacheKeyAndForwardedToOrigin": {
"EnableAcceptEncodingGzip": true,
"EnableAcceptEncodingBrotli": true,
"HeadersConfig": {"HeaderBehavior": "none"},
"CookiesConfig": {"CookieBehavior": "none"},
"QueryStringsConfig": {
"QueryStringBehavior": "whitelist",
"QueryStrings": {"Quantity": 2, "Items": ["page", "limit"]}
}
}
}'
# The cache key determines when CloudFront serves a cached response
# vs fetching from the origin. Including more parameters (headers,
# cookies, query strings) in the cache key means more cache misses.
# Include ONLY what actually changes the response.
# Route different URL paths to different origins:
#
# /api/* --> ALB (not cached, all headers forwarded)
# /* --> S3 (cached with CachingOptimized, default)
#
# In the distribution config, define multiple origins:
# "Origins": {
# "Quantity": 2,
# "Items": [
# {
# "Id": "s3-origin",
# "DomainName": "my-site-bucket.s3.us-east-1.amazonaws.com",
# "OriginAccessControlId": "E2QWRUHAPOMQZL",
# "S3OriginConfig": {"OriginAccessIdentity": ""}
# },
# {
# "Id": "alb-origin",
# "DomainName": "my-alb-123456.us-east-1.elb.amazonaws.com",
# "CustomOriginConfig": {
# "HTTPPort": 80,
# "HTTPSPort": 443,
# "OriginProtocolPolicy": "https-only",
# "OriginSslProtocols": {"Quantity": 1, "Items": ["TLSv1.2"]}
# }
# }
# ]
# }
#
# Add a cache behavior for the API path:
# "CacheBehaviors": {
# "Quantity": 1,
# "Items": [{
# "PathPattern": "/api/*",
# "TargetOriginId": "alb-origin",
# "ViewerProtocolPolicy": "https-only",
# "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
# "OriginRequestPolicyId": "216adef6-5c7f-47e4-b989-5492eafa07d3",
# "AllowedMethods": {
# "Quantity": 7,
# "Items": ["GET","HEAD","OPTIONS","PUT","POST","PATCH","DELETE"],
# "CachedMethods": {"Quantity": 2, "Items": ["GET","HEAD"]}
# }
# }]
# }
#
# CachingDisabled (4135ea2d...) forwards every request to the origin.
# AllViewer origin request policy (216adef6...) passes all viewer headers
# to the ALB so it receives the full request context.
# DefaultCacheBehavior (/*) handles everything else with S3 + CachingOptimized.
# Origin groups provide automatic failover. If the primary origin returns
# a 5xx error or times out, CloudFront retries against the secondary origin.
#
# "OriginGroups": {
# "Quantity": 1,
# "Items": [{
# "Id": "s3-failover-group",
# "FailoverCriteria": {
# "StatusCodes": {"Quantity": 4, "Items": [500, 502, 503, 504]}
# },
# "Members": {
# "Quantity": 2,
# "Items": [
# {"OriginId": "s3-primary-us-east-1"},
# {"OriginId": "s3-secondary-eu-west-1"}
# ]
# }
# }]
# }
#
# Both origins must be defined in the Origins section.
# The DefaultCacheBehavior targets the origin GROUP:
# "TargetOriginId": "s3-failover-group"
#
# Common use case: S3 buckets in different regions with
# cross-region replication enabled for disaster recovery.
# CloudFront Functions run lightweight JavaScript at edge locations.
# - Sub-millisecond execution
# - Viewer-request and viewer-response events only
# - 10 KB max package size, 2 MB max memory
# - JavaScript (cloudfront-js-2.0 runtime)
# - $0.10 per million invocations (2 million/month always free)
#
# Use cases: URL rewrites, redirects, header manipulation, simple auth
// spa-rewrite.js -- rewrite SPA paths to index.html
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri.endsWith('/')) {
request.uri += 'index.html';
} else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
# Create the function
aws cloudfront create-function \
--name spa-url-rewrite \
--function-config '{"Comment":"Rewrite SPA paths","Runtime":"cloudfront-js-2.0"}' \
--function-code fileb://spa-rewrite.js
# Publish the function (functions are created in DEVELOPMENT stage)
aws cloudfront publish-function \
--name spa-url-rewrite \
--if-match ETVPDKIKX0DER
# Associate with a cache behavior in the distribution config:
# "FunctionAssociations": {
# "Quantity": 1,
# "Items": [{
# "FunctionARN": "arn:aws:cloudfront::123456789012:function/spa-url-rewrite",
# "EventType": "viewer-request"
# }]
# }
# Lambda@Edge runs full Lambda functions at CloudFront edge locations.
# - Viewer triggers: 5s timeout, 128 MB memory
# - Origin triggers: 30s timeout, up to 10,240 MB memory
# - Supports Node.js 20.x and Python 3.12
# - MUST be created in us-east-1 (replicated to edges automatically)
# - $0.60 per million requests + duration charges
#
# Use cases: advanced auth, A/B testing, dynamic content generation,
# image transformation, security headers
// security-headers/index.mjs -- deploy to us-east-1 as origin-response trigger
export const handler = async (event) => {
const response = event.Records[0].cf.response;
const headers = response.headers;
headers['strict-transport-security'] = [{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
}];
headers['x-content-type-options'] = [{
key: 'X-Content-Type-Options',
value: 'nosniff'
}];
headers['x-frame-options'] = [{
key: 'X-Frame-Options',
value: 'DENY'
}];
headers['referrer-policy'] = [{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
}];
return response;
};
# Alternatively, use the managed SecurityHeadersPolicy response headers policy
# (67f7725c-6f97-4210-82d7-5512b31e9d03) to add security headers without code.
# Add "ResponseHeadersPolicyId" to the cache behavior in your distribution config.
#
# CloudFront Functions vs Lambda@Edge:
#
# Feature CloudFront Functions Lambda@Edge
# -------------------------------------------------------------------
# Runtime JavaScript only Node.js, Python
# Execution time Sub-millisecond 5s (viewer) / 30s (origin)
# Memory 2 MB 128 MB - 10,240 MB
# Network access Yes (cloudfront-js-2.0) Yes
# Event types Viewer only Viewer + Origin
# Pricing $0.10/million $0.60/million + duration
# Package size 10 KB 50 MB (250 MB with layers)
# Deploy region Global (automatic) us-east-1 (replicated)
#
# Use CloudFront Functions for simple request/response manipulation.
# Use Lambda@Edge for origin triggers, longer execution, or Python.
# Associate a WAF web ACL with CloudFront to block malicious requests.
# WAF for CloudFront MUST be created in us-east-1.
aws wafv2 create-web-acl \
--name cloudfront-protection \
--scope CLOUDFRONT \
--region us-east-1 \
--default-action '{"Allow":{}}' \
--rules '[
{
"Name": "AWSManagedCommonRuleSet",
"Priority": 1,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet"
}
},
"OverrideAction": {"None":{}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "CommonRuleSet"
}
},
{
"Name": "RateLimit",
"Priority": 2,
"Statement": {
"RateBasedStatement": {
"Limit": 2000,
"AggregateKeyType": "IP"
}
},
"Action": {"Block":{}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "RateLimit"
}
}
]' \
--visibility-config '{
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "cloudfront-waf"
}'
# RateBasedStatement Limit: max requests in any 5-minute window per IP.
# 2000 = roughly 400 requests/minute per IP before blocking.
# Block or allow access by country using ISO 3166-1 alpha-2 codes.
# Add to the distribution config:
#
# "Restrictions": {
# "GeoRestriction": {
# "RestrictionType": "whitelist",
# "Quantity": 3,
# "Items": ["US", "CA", "GB"]
# }
# }
#
# RestrictionType: "whitelist" (allow only listed countries),
# "blacklist" (block listed countries), or "none" (no restriction).
# CloudFront uses a GeoIP database to determine the viewer's country.
# Signed URLs restrict access to specific objects for a limited time.
# Use for premium content, temporary downloads, or time-limited access.
# Step 1: Create a public key
aws cloudfront create-public-key --public-key-config '{
"CallerReference": "signing-key-2025",
"Name": "content-signing-key",
"EncodedKey": "-----BEGIN PUBLIC KEY-----
MIIBI...
-----END PUBLIC KEY-----"
}'
# Step 2: Create a key group
aws cloudfront create-key-group --key-group-config '{
"Name": "content-signers",
"Items": ["K1A2B3C4D5E6F7"],
"Comment": "Key group for signed URLs"
}'
# Step 3: Add TrustedKeyGroups to the cache behavior:
# "TrustedKeyGroups": {
# "Enabled": true,
# "Quantity": 1,
# "Items": ["a1b2c3d4-key-group-id"]
# }
# Step 4: Generate a signed URL
aws cloudfront sign \
--url https://d111111abcdef8.cloudfront.net/premium/video.mp4 \
--key-pair-id K1A2B3C4D5E6F7 \
--private-key file://private-key.pem \
--date-less-than "2025-07-01T00:00:00Z"
# The output is a URL with a signature that expires at the specified date.
# Unsigned requests are rejected with 403.
# Invalidation removes objects from edge caches before their TTL expires.
# Invalidate specific paths
aws cloudfront create-invalidation \
--distribution-id E1A2B3C4D5E6F7 \
--paths '/index.html' '/css/main.css' '/js/app.js'
# Invalidate everything
aws cloudfront create-invalidation \
--distribution-id E1A2B3C4D5E6F7 \
--paths '/*'
# Check invalidation status
aws cloudfront get-invalidation \
--distribution-id E1A2B3C4D5E6F7 \
--id I1A2B3C4D5E6F7
# Pricing:
# - First 1,000 invalidation paths per month: free
# - Each additional path: $0.005
# - Wildcard /* counts as 1 path
# - Invalidation propagates to all edge locations in 5-15 minutes
#
# Best practice: use versioned file names (main.abc123.js) instead of
# invalidation. A new file name is a different cache key, so the update
# is immediate with no invalidation delay or cost. Reserve invalidation
# for index.html and other files that cannot be versioned.
# Origin Shield adds a third caching layer between regional edge caches
# and your origin. All cache misses go through Origin Shield first,
# collapsing duplicate origin requests.
#
# Without Origin Shield:
# [Edge] --> [Regional 1] --> [Origin]
# [Edge] --> [Regional 2] --> [Origin] (same object, two origin requests)
#
# With Origin Shield:
# [Edge] --> [Regional 1] --> [Origin Shield] --> [Origin]
# [Edge] --> [Regional 2] --> [Origin Shield] (cache hit at Shield)
#
# Enable in the origin config:
# "OriginShield": {
# "Enabled": true,
# "OriginShieldRegion": "us-east-1"
# }
#
# Choose the region closest to your origin.
# Additional cost: $0.0090 per 10,000 requests (US/Europe).
# Worth it for high-traffic distributions or expensive origin compute.
# Compression:
# CloudFront compresses automatically when:
# 1. Cache behavior has Compress: true
# 2. Viewer sends Accept-Encoding: gzip or br
# 3. Object is between 1,000 bytes and 10 MB
# 4. Content-Type is compressible (HTML, CSS, JS, JSON, XML, SVG)
#
# CachingOptimized policy includes gzip and Brotli in the cache key,
# so compressed and uncompressed versions are cached separately.
# Brotli achieves 15-25% better compression than gzip for text content.
# HTTP/3 (QUIC):
# Enabled with HttpVersion: "http2and3" in the distribution config.
# Reduces connection setup time compared to HTTP/2 (fewer round trips).
# Handles network transitions (Wi-Fi to cellular) without dropping connections.
# Price classes trade global coverage for lower costs:
#
# PriceClass_All: All edge locations worldwide (best performance, default)
# PriceClass_200: US, Canada, Europe, Asia, Middle East, Africa
# PriceClass_100: US, Canada, Europe (lowest cost)
#
# Users outside included regions still access content, but CloudFront
# routes them to the nearest INCLUDED edge location (higher latency).
# Pricing (us-east-1, 2025):
#
# Data transfer out to internet:
# First 10 TB/month: $0.085/GB
# Next 40 TB: $0.080/GB
# Next 100 TB: $0.060/GB
# Next 350 TB: $0.040/GB
#
# Requests (US/Europe):
# HTTP: $0.0075 per 10,000 ($0.75 per million)
# HTTPS: $0.0100 per 10,000 ($1.00 per million)
#
# Free tier (always free, not limited to 12 months):
# 1 TB data transfer out per month
# 10,000,000 HTTP/HTTPS requests per month
# 2,000,000 CloudFront Functions invocations per month
#
# Origin Shield: $0.0090 per 10,000 requests (US/Europe)
# Lambda@Edge: $0.60 per million requests + duration
# CloudFront Functions: $0.10 per million invocations
# Invalidation: first 1,000 paths/month free, $0.005/path after
#
# S3 origin data transfer to CloudFront: free.
# Custom origin data transfer to CloudFront: standard data transfer rates.
#
# Example: 2 TB transfer, 50 million HTTPS requests/month
# Data: (1 TB free) + 1 TB * $0.085/GB * 1024 GB = $87.04
# Requests: (10M free) + 40M * $1.00/M = $40.00
# Total: ~$127/month
# CloudFront metrics in CloudWatch (all reported in us-east-1):
#
# Requests -- total viewer requests
# BytesDownloaded -- data transferred to viewers
# BytesUploaded -- data from viewers (POST/PUT)
# TotalErrorRate -- percentage of 4xx and 5xx responses
# 4xxErrorRate -- percentage of 4xx responses
# 5xxErrorRate -- percentage of 5xx responses
# CacheHitRate -- percentage of requests served from cache
# OriginLatency -- time for CloudFront to get a response from origin
# Alarm: cache hit rate dropped below 80%
aws cloudwatch put-metric-alarm \
--alarm-name cloudfront-cache-hit-low \
--namespace AWS/CloudFront \
--metric-name CacheHitRate \
--dimensions Name=DistributionId,Value=E1A2B3C4D5E6F7 Name=Region,Value=Global \
--statistic Average \
--period 3600 \
--evaluation-periods 3 \
--threshold 80 \
--comparison-operator LessThanThreshold \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts \
--region us-east-1
# Alarm: 5xx error rate above 5%
aws cloudwatch put-metric-alarm \
--alarm-name cloudfront-5xx-high \
--namespace AWS/CloudFront \
--metric-name 5xxErrorRate \
--dimensions Name=DistributionId,Value=E1A2B3C4D5E6F7 Name=Region,Value=Global \
--statistic Average \
--period 300 \
--evaluation-periods 2 \
--threshold 5 \
--comparison-operator GreaterThanThreshold \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts \
--region us-east-1
# Access logs: delivered to S3, free, approximately 1-hour delay.
# Enable in the distribution config:
# "Logging": {
# "Enabled": true,
# "IncludeCookies": false,
# "Bucket": "my-cloudfront-logs.s3.amazonaws.com",
# "Prefix": "cdn/"
# }
#
# Real-time logs: delivered to Kinesis Data Streams with sub-minute latency.
# Use for real-time dashboards and alerting.
ViewerProtocolPolicy: redirect-to-https and set MinimumProtocolVersion: TLSv1.2_2021.AWS:SourceArn matching your specific distribution ARN.HttpVersion: http2and3) for faster connection setup, especially on mobile networks.Compress: true) with CachingOptimized to serve gzip and Brotli automatically.This article is just the start. Get the full picture with our free whitepaper - 8 chapters covering IAM, S3, VPC, monitoring, agentic AI security, compliance, and a prioritized action plan with 50+ CLI commands.
Six production-proven AWS architecture patterns: three-tier web apps, serverless APIs, event-driven processing, static websites, data lakes, and multi-region disaster recovery with diagrams and implementation guides.
Complete guide to AWS cost optimization covering Cost Explorer, Compute Optimizer, Savings Plans, Spot Instances, S3 lifecycle policies, gp2 to gp3 migration, scheduling, budgets, and production best practices.
Complete guide to AWS AI services including Rekognition, Comprehend, Textract, Polly, Translate, Transcribe, and Bedrock with CLI commands, pricing, and production best practices.