Server

CloudFront Cache Behaviors, Explained

By Prabath Thalangama· October 24, 2026· 4 min read
#aws#cloudfront#caching

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:

  1. If the origin sends Cache-Control: s-maxage=N → use N.
  2. Else if Cache-Control: max-age=N → use N (clamped to the policy's min/max TTL).
  3. Else the cache policy's default TTL.
  4. 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 only index.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 sends Cache-Control: no-cache/private/Set-Cookie, or Vary: * / 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-cache on index.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 Host header 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 fetch or require fails. Move to Lambda@Edge.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.