CloudFront is a cache in front of your origin, spread across hundreds of edge locations. It makes things faster, cheaper and more secure — and if you cannot explain what it is caching and for how long, it will eventually serve someone the wrong page.
This site runs entirely on it: a private S3 bucket, a distribution, and a small function at the edge doing URL rewriting.
The pieces
- Origin — where the real content is. S3, an ALB, an API Gateway, or any public HTTP server
- Behaviour — a path pattern and the rules for it. Behaviours are matched in order; the default catches everything else
- Cache policy — the TTLs, and which parts of the request form the cache key
- Origin request policy — what gets forwarded to the origin, which is a separate question
That split trips people up. A header can be forwarded to the origin without being part of the
cache key, which is usually what you want for something like User-Agent — the origin
may want it, but caching separately per user agent would shatter your hit rate.
The cache key is the whole game
CloudFront caches per unique cache key. Anything you add to the key multiplies the number of stored copies and divides your hit rate.
The default is the URL only. Add a query string that varies per user — a tracking parameter, a cache-buster — and every visitor gets their own copy, so your cache does nothing while costing money. The fix is to include only the query strings that genuinely change the response:
aws cloudfront get-cache-policy-config --id 4135ea2d-6df8-44a3-9df3-4b5a84be39adUse the managed policies where they fit. CachingOptimized is the sensible default
for static assets; CachingDisabled is right for an API behaviour you are only routing,
not caching.
Origin Access Control
OAC lets CloudFront read a private S3 bucket by signing its requests. The bucket keeps Block Public Access on and grants access to the distribution alone — see the S3 post for the policy.
It replaces Origin Access Identity, which still works and should not be used for anything new: OAC supports SSE-KMS encrypted objects and all regions, OAI does not.
Invalidation, and why you should rarely need it
aws cloudfront create-invalidation \
--distribution-id E30YUPLP37MY9U --paths "/*"That clears the cache. It is also asynchronous — usually a minute or two, not instant — and the
first 1,000 paths a month are free, after which each path is billed. A deploy pipeline that
invalidates /* on every push is treating a cache as if it were a filesystem.
The better answer is fingerprinted filenames. If a bundle is
app.9f2c1a.js, a new build produces a new name, which cannot be stale — so those files
get a one-year TTL and are never invalidated. Only the HTML that references them needs a short TTL,
and there is far less of it.
That is the split this site uses: fingerprinted assets under _next/static/ cached
for a year, HTML with must-revalidate.
The certificate must be in us-east-1
CloudFront only reads ACM certificates from us-east-1, regardless of where your origin, your bucket or you are. A certificate issued in your working region is simply not offered in the distribution's dropdown, with no explanation.
This catches everyone once. Note it applies to CloudFront only — an ALB or an API Gateway custom domain wants a certificate in its own region, so a stack with both needs two certificates for the same names.
Functions at the edge
Two options, and the cheap one is usually right.
CloudFront Functions are JavaScript, run in under a millisecond at the edge location, and can only touch request and response headers and the URL. No network, no filesystem. Ideal for rewriting URLs, adding security headers, or a redirect map.
Lambda@Edge is a real Lambda at the regional edge — slower, more expensive, and able to make network calls. Use it only when you need that.
This site uses a CloudFront Function to map clean URLs onto the static files behind them and to serve a set of legacy redirects.
Republish it on every deploy
The redirect map is compiled into the function. Adding a redirect in the build changes the source, and nothing at all happens at the edge until the function is republished to the LIVE stage. The deploy succeeds, the file is updated, and the redirect does not exist.
aws cloudfront describe-function --name lovemesomecoding-router --stage LIVE \
--query "FunctionSummary.FunctionMetadata.LastModifiedTime"If that timestamp is older than your last deploy, the edge is running old code.
Error pages and single-page apps
A static export serves real files, so a missing path returns S3's 403 or 404 straight to the
visitor as XML. Custom error responses fix that, and they are also how a single-page application
works at all — map 403 and 404 to /index.html with a 200 so the client-side router
sees the path.
Be careful applying that pattern to a multi-page static site: mapping every 404 to a 200 means genuinely missing pages return success, which search engines index as duplicate content. Map to a real 404 page with a 404 status instead, and let the router pattern apply only where there is actually a single-page app.
Debugging a wrong response
Check the response headers first:
curl -sSI https://lovemesomecoding.com/aws | grep -i "x-cache\|age\|cache-control"X-Cache: Hit from cloudfront means you are seeing cached content, and
Age tells you how long it has been there — so a stale page with a large
Age is a TTL question, not an origin bug. Miss from cloudfront means the
request reached your origin and the problem is behind CloudFront, not in front of it.