CloudFront is AWS's CDN: copies of your content are cached at edge locations close to viewers, so most requests never reach your origin. Overview is in 5. AWS Networking; header hardening is in 3. HTTP Security Headers and CSP.
Parts at a glance
| Part | Role |
|---|---|
| Distribution | The CloudFront config: domain dxxxx.cloudfront.net, origins, behaviors, certificate |
| Origin | Where content really lives: S3, ALB, EC2, API Gateway, any HTTP server, Lambda function URL |
| Origin group | Primary + secondary origin for failover |
| Behavior | A path pattern (/api/*, default *) and the rules for it |
| Cache policy | What goes in the cache key and the TTLs |
| Origin request policy | What extra data is forwarded to the origin (not in the cache key) |
| Response headers policy | Headers CloudFront adds or overrides on the way back |
| Edge function | CloudFront Functions or Lambda@Edge for code at the edge |
| Invalidation | Remove cached objects before their TTL ends |
| Edge location / Regional edge cache | Where objects are cached, and a larger middle tier behind them |
How a Request Flows
sequenceDiagram
participant V as Viewer
participant E as Edge location
participant R as Regional edge cache
participant O as Origin (S3 / ALB)
V->>E: GET /img/logo.png
alt In edge cache (fresh)
E-->>V: 200 (X-Cache: Hit from cloudfront)
else Miss
E->>R: Request
alt In regional cache
R-->>E: Object
else Miss
R->>O: Request (origin request policy applied)
O-->>R: Object + Cache-Control
end
E-->>V: 200 (X-Cache: Miss from cloudfront)
end
Identical requests are collapsed: if 100 viewers ask for the same uncached object at once, one request goes to the origin.
Origins
| Origin type | Notes |
|---|---|
| S3 bucket | Use the bucket REST endpoint with Origin Access Control (OAC) so the bucket stays private |
| S3 website endpoint | Treated as a custom (HTTP-only) origin; needed for S3 redirects and index handling; bucket must be public, so prefer OAC |
| ALB / EC2 / any server | A custom origin: you choose HTTP or HTTPS to the origin and ports |
| API Gateway | Regional API as a custom origin |
| Lambda function URL | Supported, with OAC for IAM-protected URLs |
| VPC origin | Reach an ALB / NLB / EC2 in a private subnet without making it public |
Origin settings worth knowing:
- Protocol policy: HTTP only, HTTPS only, or match viewer. Use HTTPS to the origin
- Origin path: prefix added to every request (e.g. /v2)
- Custom headers: static headers CloudFront adds to every origin request (for example a secret header the ALB checks)
- Timeouts: connection timeout / attempts, response (read) timeout, keep-alive timeout
- Origin Shield: an extra caching layer in one chosen Region in front of the origin. Raises hit ratio and cuts origin load when you have viewers worldwide; adds a small per-request cost
Origin groups (failover)
Two origins form a group; CloudFront retries the secondary when the primary returns chosen status codes (for example 500, 502, 503, 504, or 403/404) or times out. Failover applies to GET, HEAD, OPTIONS. Useful for S3 cross-Region replicas.
Keep the origin private
flowchart LR
V[Viewer] --> CF[CloudFront]
CF -- OAC, signed SigV4 --> S3[(Private S3 bucket)]
CF -- secret header + HTTPS --> ALB[ALB]
X[Direct internet] -. blocked .-> S3
X -. blocked .-> ALB
- S3: bucket policy lets only the CloudFront service principal read, restricted to your distribution:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-unique-bucket-12345/*",
"Condition": { "StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE" } }
}]
}
- ALB / custom origin: use a VPC origin, or allow only the CloudFront origin-facing managed prefix list (
com.amazonaws.global.cloudfront.origin_facing) in the security group, and optionally check a secret custom header - OAC replaces the older OAI; use OAC for new work (supports SSE-KMS and all Regions)
Behaviors
A distribution has a default behavior (*) plus extra behaviors for specific paths. The most specific pattern wins by precedence order, so order matters.
| Setting | Options |
|---|---|
| Path pattern | /static/*, /api/*, *.jpg |
| Origin | Which origin serves this path |
| Viewer protocol policy | HTTP and HTTPS, redirect HTTP to HTTPS, HTTPS only |
| Allowed methods | GET, HEAD; add OPTIONS; or all (POST, PUT, DELETE...) for APIs |
| Cache policy / Origin request policy / Response headers policy | See below |
| Compress | Gzip and Brotli for compressible types |
| Function associations | Viewer request / response, origin request / response |
| Restrict viewer access | Signed URLs or cookies via trusted key groups |
Typical layout: /static/* to S3 with long caching; /api/* to an ALB with caching disabled; default to the app.
Caching in Depth
The cache key
The cache key decides whether two requests share one cached object. It always includes the URL path and the distribution domain, plus whatever the cache policy adds: chosen headers, cookies, query strings.
- Fewer cache-key values = higher hit ratio. Do not include
User-Agent, all cookies or all query strings unless the content truly varies by them - A header, cookie or query string can be forwarded to the origin without being in the cache key using an origin request policy. This is the clean way to pass
Host,Authorization, or tracking cookies the origin needs without fragmenting the cache
Cache policy: TTLs
| TTL | Meaning |
|---|---|
| Minimum TTL | Lowest time to cache, even if the origin says less |
| Default TTL | Used when the origin sends no Cache-Control / Expires (24 h if unset) |
| Maximum TTL | Cap, even if the origin asks for longer |
How the origin's headers interact:
- Cache-Control: max-age=N or s-maxage=N (s-maxage wins for shared caches) sets the TTL, clamped between Minimum and Maximum
- Cache-Control: no-cache, no-store or private is respected when Minimum TTL is 0; setting a Minimum TTL above 0 overrides them
- No headers: Default TTL applies
Managed policies
| Policy | Use |
|---|---|
Managed-CachingOptimized |
Static content: long TTL, minimal cache key, gzip + Brotli |
Managed-CachingDisabled |
APIs and dynamic pages: no caching |
Managed-AllViewer (origin request) |
Forward all viewer headers, cookies and query strings to the origin |
Managed-CORS-S3Origin (origin request) |
Forward the headers S3 needs for CORS |
Look up IDs with aws cloudfront list-cache-policies --type managed.
Setting good cache headers at the origin
# Fingerprinted assets (app.3f9c2a.js): cache for a year
Cache-Control: public, max-age=31536000, immutable
# HTML pages: always revalidate
Cache-Control: no-cache
# Shared cache longer than the browser
Cache-Control: public, max-age=60, s-maxage=3600
# Serve stale while refreshing
Cache-Control: max-age=60, stale-while-revalidate=300, stale-if-error=86400
For S3, set headers when uploading:
$ aws s3 cp ./dist s3://my-unique-bucket-12345 --recursive \
--cache-control "public, max-age=31536000, immutable" \
--exclude "*.html"
$ aws s3 cp ./dist s3://my-unique-bucket-12345 --recursive \
--cache-control "no-cache" --exclude "*" --include "*.html"
Best practice: fingerprint filenames (app.<hash>.js) so you can cache for a year and never need to invalidate; keep HTML short-lived.
Custom error responses
| Setting | Use |
|---|---|
Map 403 / 404 to /index.html with 200 |
Single-page apps (client-side routing) |
| Error caching minimum TTL (default 10 s) | How long errors are cached; lower it while debugging |
| Custom error page per status | Branded 404 / 503 pages from S3 |
Inspecting cache behaviour
$ curl -sI https://www.example.com/app.js | grep -iE 'x-cache|age|via|x-amz-cf'
x-cache: Hit from cloudfront
age: 4721
x-amz-cf-pop: JNB50-C1
x-amz-cf-id: ...
X-Cache values: Hit, Miss, RefreshHit (revalidated with origin), Error. Age is seconds in cache. You can also add additional metrics (cache hit rate, origin latency) in CloudWatch.
Response Headers Policy: Add and Override Headers
Lets CloudFront set headers on every response without touching the origin, useful for S3 and static sites that cannot add headers.
| Type | Examples |
|---|---|
| Security headers | Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-Policy |
| CORS | Access-Control-Allow-Origin, methods, headers, max age, credentials |
| Custom headers | Any name and value, e.g. Permissions-Policy, Cache-Control |
| Remove headers | Strip Server, X-Powered-By |
| Server-Timing | Add timing data for debugging, with a sampling rate |
Each header has an Origin override switch: - On: CloudFront's value replaces the origin's value - Off: CloudFront adds its value only if the origin did not send that header
Managed policy Managed-SecurityHeadersPolicy gives a sensible baseline; build a custom policy for your own CSP. Validate the result with the commands in 3. HTTP Security Headers and CSP.
Two related tools: - Origin custom headers: sent to the origin (request side) - CloudFront Functions / Lambda@Edge: for logic (conditional headers, rewrites)
Domains and HTTPS
flowchart LR
U[www.example.com] --> R53[Route 53 alias record A/AAAA]
R53 --> CF[CloudFront distribution]
CF -. certificate .-> ACM[ACM cert in us-east-1]
- Request a certificate in ACM in
us-east-1(required for CloudFront) forexample.comandwww.example.com; validate by DNS - Add the names as Alternate domain names (CNAMEs) on the distribution and attach the certificate
- In DNS, point the name to the distribution: in Route 53 use an alias A and AAAA record (works at the zone apex); elsewhere use a CNAME to
dxxxx.cloudfront.net(AWS Route 53 Deep Dive) - Pick the security policy (minimum TLS), e.g.
TLSv1.2_2021 - Use SNI (free) unless you must support very old clients (dedicated IP costs extra)
Also: default root object (index.html) applies only to the root URL, not sub-folders; HTTP/2 and HTTP/3 can be enabled; IPv6 is on by default.
Invalidations: Flushing the Cache
$ aws cloudfront create-invalidation \
--distribution-id EDFDVBD6EXAMPLE \
--paths "/index.html" "/images/*"
$ aws cloudfront list-invalidations --distribution-id EDFDVBD6EXAMPLE
$ aws cloudfront get-invalidation --distribution-id EDFDVBD6EXAMPLE --id I2J0I21PCUYOIK
- Path patterns:
/file.html,/folder/*, or/*for everything. A wildcard counts as one path - Typically completes in minutes, not instantly
- A free monthly allowance of paths, then a per-path charge. Invalidating
/*once per deploy is cheap; invalidating thousands of individual paths is not - Query strings are part of the object identity unless your cache key ignores them
- Better than invalidating: use versioned filenames, short HTML TTLs, or a new path prefix per release
- Invalidation clears CloudFront only. Browsers keep their own copy until
max-ageends, so set sensibleCache-Control
CI step after a deploy (GitHub Actions):
- run: aws s3 sync ./dist s3://my-unique-bucket-12345 --delete
- run: aws cloudfront create-invalidation --distribution-id ${{ vars.CF_DIST_ID }} --paths "/*"
Large Uploads and Timeouts End to End
Uploads of tens of MB can fail silently because each hop has its own timeout: CloudFront origin response timeout, ALB idle timeout (default 60 s), the app server (Gunicorn --timeout, default 30 s), and WAF body-size limits. Tune every layer consistently, and prefer presigned S3 uploads so big files bypass the app tier. Worked example: Django on ECS Fargate - Architecture and Lessons.
Edge Compute
| Feature | CloudFront Functions | Lambda@Edge |
|---|---|---|
| Runtime | JavaScript (small, fast) | Node.js, Python |
| Runs at | Edge location | Regional edge cache |
| Events | Viewer request / response | Viewer and origin request / response |
| Time / size | Sub-millisecond, tiny limits | Seconds, larger packages |
| Network / body access | No | Yes (can call APIs, read body) |
| Cost | Much cheaper | Higher |
| Typical use | URL rewrites, redirects, header tweaks, simple auth checks, cache-key normalisation | A/B tests, auth with external calls, image resizing, dynamic origin selection |
// CloudFront Function: serve /about as /about/index.html
function handler(event) {
var request = event.request;
if (request.uri.endsWith('/')) request.uri += 'index.html';
else if (!request.uri.includes('.')) request.uri += '/index.html';
return request;
}
Lambda@Edge is authored in us-east-1 and replicated.
Security
| Control | How |
|---|---|
| HTTPS | Redirect to HTTPS, modern TLS policy, HSTS via response headers policy |
| Private origins | OAC for S3; VPC origins or prefix list for ALB |
| WAF | Attach a global web ACL (created in us-east-1): rate limits, managed rule groups, bot control |
| DDoS | AWS Shield Standard included; Shield Advanced optional |
| Signed URLs / cookies | Restrict private content to authorised users, with expiry; signed by a trusted key group |
| Geo restriction | Allow-list or block-list countries |
| Field-level encryption | Encrypt chosen form fields at the edge |
| Referer / token checks | Edge function or WAF rule to stop hotlinking |
Signed URL for a private object:
$ aws cloudfront sign --url https://www.example.com/private/report.pdf \
--key-pair-id K2JCJMDEHXQW5F --private-key file://private_key.pem \
--date-less-than 2026-12-31
Logging and Monitoring
- Standard logs to S3 (or CloudWatch Logs / Firehose): per-request records, delayed by minutes
- Real-time logs to Kinesis Data Streams: seconds-level, sampled
- CloudWatch metrics (free, in
us-east-1): requests, bytes, 4xx / 5xx error rate; enable additional metrics for cache hit rate and origin latency - Alarm on rising 5xx and falling cache hit ratio
Cost and Choices
- Charged for data transfer out and requests (by Region), plus function invocations, invalidations beyond the free allowance, and WAF. Origin fetch from S3 to CloudFront is free
- Price classes: All edge locations, 200, or 100 (cheapest Regions only)
- A permanent free tier covers a modest amount of monthly transfer and requests
- Continuous deployment lets you test a config change on a small slice of traffic with a staging distribution
Example: Static Site on S3 (Terraform)
data "aws_cloudfront_cache_policy" "optimized" {
name = "Managed-CachingOptimized"
}
data "aws_cloudfront_response_headers_policy" "security" {
name = "Managed-SecurityHeadersPolicy"
}
resource "aws_cloudfront_origin_access_control" "site" {
name = "site-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
resource "aws_cloudfront_distribution" "site" {
enabled = true
default_root_object = "index.html"
aliases = ["www.example.com"]
price_class = "PriceClass_100"
origin {
domain_name = aws_s3_bucket.site.bucket_regional_domain_name
origin_id = "s3-site"
origin_access_control_id = aws_cloudfront_origin_access_control.site.id
}
default_cache_behavior {
target_origin_id = "s3-site"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
compress = true
cache_policy_id = data.aws_cloudfront_cache_policy.optimized.id
response_headers_policy_id = data.aws_cloudfront_response_headers_policy.security.id
}
restrictions {
geo_restriction { restriction_type = "none" }
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.site.arn # us-east-1
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
}
More on state and modules: Terraform, Terraform Best Practices.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Old content still showing | TTL not expired or browser cache; invalidate, or version the filenames |
Always Miss |
Cache key includes too much (cookies, query strings, Authorization), or origin sends no-store / private |
403 from S3 origin |
OAC / bucket policy missing, wrong bucket endpoint, or object absent (S3 returns 403 for missing keys without list permission) |
502 |
TLS handshake to origin failed (certificate name mismatch, unsupported protocol) |
504 |
Origin too slow; raise the origin response timeout or fix the origin |
403 on custom domain |
Alternate domain name missing on the distribution, or wrong certificate |
| Cert not selectable | ACM certificate not in us-east-1 |
| CORS errors | Forward Origin (origin request policy) or add a CORS response headers policy |
| Sub-folder returns error | Default root object only covers /; add a function to append index.html |
| Changes not live yet | Distribution status still "Deploying" |