CloudFront Cache Behaviors, Explained
Introduction¶
A CloudFront distribution has one or more cache behaviors, each a
path pattern → origin mapping with its own caching and request-handling
rules. Requests match the most specific behavior; the default (*) catches the
rest.
Behaviors and precedence¶
/api/* -> ALB origin, no caching, forward all headers/cookies/query
/static/* -> S3 origin, cache 1 year, no cookies
/images/* -> S3 origin, cache 1 day
* (default) -> ALB origin, cache 5 min, forward Host + a few headers
Behaviors are evaluated by precedence order (you set it); the first pattern
that matches wins. /api/* must come before *. Patterns support * and ?
wildcards; no regex.
The cache key¶
CloudFront caches a response under a cache key — by default just the URL path. What else goes into the key is controlled by a cache policy:
- Query strings — none / all / a specific list. Including a marketing
utm_*param in the key fragments your cache pointlessly; exclude those. - Headers — none / a specific list (e.g.
Accept,Accept-Language,CloudFront-Viewer-Country). Every header in the key multiplies cache variants. - Cookies — none / all / a list. Any cookie in the cache key on a high-cardinality cookie (a session ID) makes the cache useless.
Keep the cache key minimal — only what genuinely changes the response.
Cache policy vs origin request policy¶
Two separate things, easy to confuse:
- Cache policy — what's in the cache key (above) + the TTLs.
- Origin request policy — what CloudFront forwards to the origin on a cache miss, without adding it to the cache key.
Example: you want the origin to see the Authorization header (to decide the
response) but you do not want to cache per-token. Put Authorization in the
origin request policy, not the cache policy — and set the cache policy to
not cache (or the app to send Cache-Control: private).
TTLs vs origin headers¶
Precedence for how long CloudFront caches an object:
- If the origin sends
Cache-Control: s-maxage=N→ use N. - Else if
Cache-Control: max-age=N→ use N (clamped to the policy's min/max TTL). - Else the cache policy's default TTL.
Cache-Control: no-store/private→ don't cache (with the right policy).
Best practice: let the origin set Cache-Control per response (long for
hashed assets, short for HTML, no-store for personalised). The cache policy's
min/max just bound it.
stale-while-revalidate / stale-if-error are honoured — serve stale while
refetching, or during an origin error.
Compression¶
Enable "Compress objects automatically" on the behavior — CloudFront gzips/brotli compressible types if the viewer supports it and the origin didn't already compress. Don't double-compress at the origin for CloudFront-served paths.
Invalidation vs versioned URLs¶
- Invalidation —
aws cloudfront create-invalidation --paths "/index.html". First 1,000 paths/month free, then billed. Takes a few minutes to propagate. Use for HTML / a few files. - Versioned URLs —
app.a1b2c3.js(hash in the filename). Deploy new asset → new URL → new cache entry, no invalidation needed. The right approach for static assets; invalidate onlyindex.html(which references them).
Edge functions¶
- CloudFront Functions — tiny JS, viewer-request/response only, sub-millisecond, cheap. URL rewrites, header manipulation, simple auth, redirects, A/B header. No network/filesystem.
- Lambda@Edge — full Lambda, all four trigger points (viewer/origin request/ response), can do network calls, heavier and pricier. Use when Functions aren't enough (fetch config, complex auth, body manipulation).
Common: a CloudFront Function on viewer-request to rewrite extensionless paths to
/index.html for an SPA, or to add security headers.
Verification and troubleshooting¶
curl -sI https://d123.cloudfront.net/static/app.a1b2c3.js | grep -iE 'x-cache|age|cache-control'
# X-Cache: Hit from cloudfront / Miss from cloudfront
aws cloudfront get-distribution-config --id EXXXX
- Everything
Miss from cloudfront— the cache key includes a high-cardinality value (a session cookie,utm_*, a per-request header), the origin sendsCache-Control: no-cache/private/Set-Cookie, orVary: */Vary: Cookie. Trim the cache policy; fix origin headers. - Stale content after deploy — no invalidation and unversioned assets, or
a long TTL on HTML. Version assets,
no-cacheonindex.html, invalidate/and/index.html. /api/*responses being cached (users see each other's data) — the behavior for/api/*doesn't exist or has caching enabled. Add a behavior with the "CachingDisabled" managed policy + an origin request policy that forwards everything.- Behavior precedence wrong —
*matched before/static/*because*had higher precedence. Reorder. - 403 from S3 origin — OAC/bucket policy (see
the S3 static site post), or
the behavior forwards the
Hostheader to S3 (don't — S3 REST endpoints reject a mismatched Host). - Gzip not applied — "Compress objects automatically" off, the type isn't in
CloudFront's compressible list, the object is < 1KB or > 10MB, or the origin
already sent
Content-Encoding. - CloudFront Function errors — they're strict ES5-ish, 1ms CPU, 2ms for
viewer-response; a
fetchorrequirefails. Move to Lambda@Edge.
Related tools and reading¶
- On-site: HTTP Header Analyzer, Redirect checker.
- Related posts: An S3 static site behind CloudFront, Nginx content caching.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Cloud & AWS
AWS WAF: The Rules That Matter
AWS WAF filters HTTP requests at the ALB/CloudFront/API Gateway edge. Here's a sensible baseline Web ACL, how to tune with count mode, and the false-positive traps.
October 24, 2026An S3 Static Site Behind CloudFront
The right way to host a static site on AWS: a private S3 bucket, CloudFront with OAC in front, HTTPS via ACM, and SPA fallback routing. Plus the 403s that trip everyone.
October 24, 2026CloudWatch Logs and Metric Filters
CloudWatch Logs is cheap to write and expensive to forget about. Here's retention, turning error patterns into metrics and alarms, and Logs Insights for ad-hoc queries.
October 24, 2026