AWS CloudFront
CloudFront in practice. Origin access control, the cache key that decides your hit rate, invalidation costs and the us-east-1 certificate rule.
Amazon CloudFront
CloudFront is a CDN: it caches your content at hundreds of edge locations so a request from Sydney is served from Sydney rather than from your origin in Ireland.
CloudFront: CDN Request Flow
Cache Hit = faster response + lower origin cost
Latency is the obvious benefit. Two less obvious ones often matter more: the origin only sees traffic that actually misses the cache, which can cut its load by an order of magnitude, and data transfer out through CloudFront is cheaper than straight from S3 or EC2, so a well-cached distribution often reduces the bill rather than adding to it.
Origins and Origin Access Control
An origin is where CloudFront fetches on a miss: an S3 bucket, an ALB, an API Gateway, or any public HTTP endpoint.
For S3, the important pattern is keeping the bucket completely private and letting only CloudFront read it. Origin Access Control is the current mechanism; Origin Access Identity is its predecessor and still appears in older guides, but OAC supports SSE-KMS and all HTTP methods, so use it for anything new.
aws cloudfront create-origin-access-control \
--origin-access-control-config '{
"Name": "site-oac",
"OriginAccessControlOriginType": "s3",
"SigningBehavior": "always",
"SigningProtocol": "sigv4"
}'
The bucket policy then grants CloudFront access, scoped to the one distribution:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-site/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E1ABCDEFGHIJKL"
}
}
}]
}
Getting this wrong produces a 403 that looks like a CloudFront problem and is a bucket policy problem. Do not enable S3 static website hosting alongside OAC, that endpoint is public HTTP only and bypasses the whole arrangement.
The cache key decides your hit rate
This is where most CloudFront disappointment comes from. The cache key is what CloudFront uses to decide whether it already has a response, and anything included in the key fragments the cache.
aws cloudfront create-cache-policy --cache-policy-config '{
"Name": "static-assets",
"DefaultTTL": 86400,
"MinTTL": 1,
"MaxTTL": 31536000,
"ParametersInCacheKeyAndForwardedToOrigin": {
"EnableAcceptEncodingGzip": true,
"EnableAcceptEncodingBrotli": true,
"HeadersConfig": {"HeaderBehavior": "none"},
"CookiesConfig": {"CookieBehavior": "none"},
"QueryStringsConfig": {"QueryStringBehavior": "none"}
}
}'
Forwarding all query strings, all cookies or all headers means a separate cache entry per unique combination. A marketing campaign appending ?utm_source=… to every link creates a fresh cache entry per variant. The content is identical, and your hit rate collapses.
Include in the key only what genuinely changes the response:
| Behaviour | Cache key |
|---|---|
Static assets (/static/*) | Nothing (path only) |
| Images with resizing | Only the dimension parameters |
| API responses | The parameters that change the result |
| Authenticated pages | Do not cache, or key on a session-agnostic value |
Use the managed policies where they fit, Managed-CachingOptimized for static assets, Managed-CachingDisabled for APIs, rather than writing your own from scratch.
Watch the hit rate, which is the single most useful CloudFront metric:
aws cloudwatch get-metric-statistics \
--namespace AWS/CloudFront --metric-name CacheHitRate \
--dimensions Name=DistributionId,Value=E1ABCDEFGHIJKL Name=Region,Value=Global \
--start-time 2026-09-15T00:00:00Z --end-time 2026-09-16T00:00:00Z \
--period 3600 --statistics Average
CloudFront metrics live in us-east-1 regardless of where anything else is.
TTLs: headers win
CloudFront honours Cache-Control from your origin, and the distribution's TTLs act as bounds:
MinTTL. Floor, even if the origin saysno-cacheDefaultTTL. Used when the origin sends no cache headerMaxTTL. Ceiling, even if the origin says a year
The clean approach is to control caching at the origin and leave CloudFront's TTLs permissive. Combined with hashed filenames, this removes invalidation from your life entirely:
Cache-Control: public, max-age=31536000, immutable → /static/app.4f3a1b.js
Cache-Control: public, max-age=0, must-revalidate → /index.html
Fingerprinted assets cache forever because a new build produces a new filename; the HTML that references them is never cached. Nothing needs invalidating on deploy.
Invalidation is not a deployment strategy
aws cloudfront create-invalidation \
--distribution-id E1ABCDEFGHIJKL --paths '/index.html' '/'
The first 1,000 paths each month are free, then each path costs money, and a wildcard /* counts as one path, which makes it cheaper than listing many files but slower, taking minutes to propagate globally.
Teams that invalidate /* on every deploy are working around a caching strategy that should have used hashed filenames. Reserve invalidation for genuine mistakes.
Custom error pages
Essential for a single-page application, because S3 returns 403 (not 404) for a missing key when using OAC, so any client-side route fails:
--custom-error-responses '{
"Quantity": 2,
"Items": [
{"ErrorCode": 403, "ResponsePagePath": "/index.html", "ResponseCode": "200", "ErrorCachingMinTTL": 10},
{"ErrorCode": 404, "ResponsePagePath": "/index.html", "ResponseCode": "200", "ErrorCachingMinTTL": 10}
]
}'
Note ErrorCachingMinTTL. The default caches errors for five minutes, so a brief origin outage is served as an error long after the origin recovers. Lower it.
HTTPS and the us-east-1 rule
An ACM certificate for CloudFront must be in us-east-1. Not your bucket's region, not your ALB's region, us-east-1, always. It is the most common CloudFront setup error, and a certificate in the wrong region simply does not appear in the list of choices.
aws acm request-certificate --region us-east-1 \
--domain-name cdn.example.com \
--validation-method DNS
Set the viewer policy to redirect and pick a modern security policy:
--default-cache-behavior '{
"ViewerProtocolPolicy": "redirect-to-https",
"Compress": true,
"AllowedMethods": {"Quantity": 2, "Items": ["GET","HEAD"]}
}' \
--viewer-certificate 'ACMCertificateArn=…,SSLSupportMethod=sni-only,MinimumProtocolVersion=TLSv1.2_2021'
"Compress": true is free and reduces transfer noticeably for text assets.
Common errors
403 ERROR — The request could not be satisfied. Access Denied
OAC is configured but the bucket policy does not allow cloudfront.amazonaws.com, or the AWS:SourceArn does not match the distribution.
InvalidViewerCertificate: The specified SSL certificate ... must be in the
us-east-1 region
Request it again in us-east-1.
CNAMEAlreadyExists: One or more of the CNAMEs you provided are already
associated with a different resource
That alternate domain name is attached to another distribution, possibly in another account.
Deploy complete, old content still served. The cache key or TTL, not a CloudFront fault. Check Cache-Control from the origin and whether the asset is fingerprinted.
Quick reference
| Task | Command |
|---|---|
| List distributions | aws cloudfront list-distributions --query 'DistributionList.Items[].[Id,DomainName]' |
| Get config | aws cloudfront get-distribution-config --id <id> |
| Invalidate | aws cloudfront create-invalidation --paths '/index.html' |
| Create OAC | aws cloudfront create-origin-access-control |
| Create a cache policy | aws cloudfront create-cache-policy |
| List managed policies | aws cloudfront list-cache-policies --type managed |
| Certificate (us-east-1) | aws acm request-certificate --region us-east-1 |
| Watch hit rate | CacheHitRate in AWS/CloudFront, us-east-1 |
Key takeaways
- Keep the S3 bucket private and use OAC, with the bucket policy scoped by
AWS:SourceArn. - The cache key sets your hit rate. Forwarding all query strings or cookies destroys it.
- Control TTLs with origin
Cache-Controland leave CloudFront's bounds permissive. - Hashed filenames plus never-cached HTML removes the need to invalidate at all.
- Invalidation is not a deploy step. It costs money beyond 1,000 paths and is slow.
- The ACM certificate must be in us-east-1. No exceptions.
- Map 403 and 404 to
/index.htmlfor an SPA, and lowerErrorCachingMinTTL. - CloudFront metrics are in us-east-1, whatever region the rest of your stack uses.
Check your understanding
A hit rate that collapsed
A distribution serving static assets had a 95% cache hit rate. After a marketing campaign started appending ?utm_source and ?utm_campaign to links, it fell to 20%. The cache policy forwards all query strings. Why?
A certificate that cannot be selected
A team requests an ACM certificate for cdn.example.com in eu-west-1, validates it successfully, and then cannot choose it for their CloudFront distribution. What is wrong?
Frequently Asked Questions
Why does my CloudFront distribution return 403 Access Denied from S3?
Almost always the bucket policy. With Origin Access Control the bucket stays private and must explicitly allow the cloudfront.amazonaws.com service principal to call s3:GetObject, normally with an AWS:SourceArn condition naming the distribution, and a mismatch there denies every request. Two other causes worth checking: S3 static website hosting enabled alongside OAC, which creates a public HTTP endpoint that bypasses the signing, and an object that genuinely does not exist, since S3 returns 403 rather than 404 when the caller lacks list permission.
How do I improve my CloudFront cache hit ratio?
Reduce what is in the cache key. Forwarding all query strings, cookies or headers creates a separate cache entry per combination, so identical content is fetched repeatedly. Tracking parameters such as utm_source are the classic cause. Set QueryStringBehavior to none for static assets, forward only the parameters that genuinely change the response, and set long Cache-Control max-age values at the origin. The Managed-CachingOptimized policy is a good default for static content.
What is the difference between OAC and OAI?
Origin Access Identity was the original way to keep an S3 bucket private while letting CloudFront read it. Origin Access Control replaces it and is what you should use for anything new: it supports SSE-KMS encrypted objects, all HTTP methods rather than just GET and HEAD, and signs requests with SigV4 scoped to a specific distribution. OAI still works for existing distributions, which is why plenty of older tutorials use it, but there is no reason to choose it today.
How much do CloudFront invalidations cost?
The first 1,000 paths each month are free, and each path after that is charged. A wildcard such as /* counts as a single path, which makes it cheaper than enumerating hundreds of files but slower to propagate, often several minutes. The wider point is that regular invalidation indicates a caching strategy problem: fingerprinted asset filenames combined with non-cached HTML means new deployments are picked up immediately with no invalidation at all.
Why is CloudFront still serving old content after I deployed?
Because the cached object has not expired, which is CloudFront working as configured rather than a fault. Check the Cache-Control header your origin returns, if it says max-age=86400, the edge will hold that object for a day regardless of what you uploaded. The durable fix is content hashing: give built assets names containing a content hash so a new build produces new URLs, and serve the HTML that references them with max-age=0, must-revalidate.
How do I make a single-page application work on CloudFront?
Add custom error responses mapping both 403 and 404 to /index.html with a response code of 200, so client-side routes resolve instead of failing. Both codes are needed because S3 with OAC returns 403 for a missing key rather than 404. Also lower ErrorCachingMinTTL from its five-minute default, otherwise a brief origin problem is cached and served as an error well after the origin has recovered.