Blog, 11 April 2023, updated 4 November 2026, AWS

Caching Static and Dynamic Content with Amazon CloudFront

How CloudFront decides what to cache

Amazon CloudFront is a content delivery network. Viewers connect to a nearby edge location, and CloudFront either answers from its cache (a cache hit) or forwards the request to your origin, such as an Amazon S3 bucket, an Application Load Balancer or any HTTP server (a cache miss).

Abstract speedometer graphic suggesting fast website performance

Three pieces of configuration decide what happens to each request:

  • Cache behaviors match request paths to settings. A distribution has a default behavior (*) and can have more, such as /static/* or /api/*. Each behavior points to an origin and carries its own policies.
  • Cache policies define the cache key (which headers, cookies and query strings make one cached object different from another) and the minimum, default and maximum TTL. They also enable caching of Gzip and Brotli compressed objects.
  • Origin request policies define what CloudFront sends to the origin on a miss, without adding it to the cache key.

The split between the last two matters. AWS documents that every value in the cache key is automatically included in origin requests, so the origin request policy is for values the origin needs but that should not fragment the cache, such as a header used only for logging. See Control origin requests with a policy.

Static content: a small cache key and long TTLs

For images, CSS, JavaScript and fonts, the goal is the highest possible cache hit ratio. Keep the cache key small: if the files do not change by cookie or query string, leave cookies and query strings out of it.

The managed CachingOptimized policy does exactly this. It includes no cookies or query strings, only the normalized Accept-Encoding header, and sets a minimum TTL of 1 second, a default of 24 hours and a maximum of 365 days. The full list is in Use managed cache policies.

The safest pattern for static assets is versioned file names, such as app.3f9c2a.js, served with a long Cache-Control lifetime. A new build produces new names, so you never need to purge the old ones.

How Cache-Control and TTLs interact

The TTLs in the cache policy and the headers your origin sends work together. The rules in Manage how long content stays in the cache are, in short:

  • If the origin sends Cache-Control: max-age, CloudFront caches for that value, clamped between the policy’s minimum and maximum TTL. Browsers use max-age as sent.
  • If the origin also sends s-maxage, CloudFront uses s-maxage (again clamped) and browsers still use max-age. This lets you cache longer at the edge than in the browser.
  • If the origin sends no max-age, CloudFront uses the default TTL (or the minimum TTL, if that is greater).
  • If both max-age and Expires are present, CloudFront uses max-age.
  • If the origin sends no-cache, no-store or private and the minimum TTL is 0, CloudFront respects them. If the minimum TTL is greater than 0, CloudFront caches the response for the minimum TTL anyway.

That last rule is the one that catches teams out. CachingOptimized has a minimum TTL of 1 second, so a personalized response marked private can still be cached for a second and served to another viewer. For any path that returns per-user content, use a cache policy with a minimum TTL of 0.

A typical split looks like this:

# Fingerprinted static asset
Cache-Control: public, max-age=31536000, immutable

# HTML page: short at the edge, revalidated in the browser
Cache-Control: public, max-age=0, s-maxage=300

# Personalized response
Cache-Control: private, no-store

CloudFront also supports stale-while-revalidate and stale-if-error, which let it serve an expired copy while it fetches a fresh one or while the origin is failing, capped by the maximum TTL.

Caching dynamic content safely

Dynamic does not have to mean uncacheable. Many API responses and HTML pages are the same for every viewer for seconds or minutes, and even a short TTL can take most of the load off the origin during a spike.

To do it safely:

  • Put dynamic paths in their own cache behavior, such as /api/*, so their settings do not leak into static paths.
  • Include in the cache key only the values the response really varies by, for example one query string parameter or a language header. Each extra value multiplies the number of cached copies.
  • Never put responses that depend on an Authorization header or session cookie into a shared cache unless that value is part of the cache key. If in doubt, use the managed CachingDisabled policy for that behavior and forward what the origin needs with an origin request policy.
  • If your origin already sets correct Cache-Control headers, the managed UseOriginCacheControlHeaders policies (minimum and default TTL 0) let the origin decide per response.

Even with caching disabled, requests still benefit from CloudFront: TLS terminates at an edge location close to the viewer, and CloudFront keeps persistent connections open to the origin.

Compression

CloudFront can compress responses with Gzip or Brotli at the edge. Set Compress objects automatically to Yes on the cache behavior and use a cache policy with Gzip and Brotli enabled; Brotli requires a cache policy rather than legacy cache settings. Per Serve compressed files, CloudFront compresses only listed content types, only objects between 1,000 and 10,000,000 bytes, and only when the origin sends a Content-Length header. If the origin already compressed the response and sends Content-Encoding, CloudFront passes it through unchanged.

Origin Shield

Without it, each regional edge cache can send its own request to your origin for the same object. Origin Shield adds one more caching layer in a Region you choose, ideally the one with the lowest latency to your origin, and consolidates those requests. It helps most with viewers spread across many regions, on-the-fly image processing, live video packaging and origins with limited capacity. AWS notes it is a poor fit for dynamic content that is proxied to the origin or rarely requested. It is charged per request, so estimate the cost first.

Invalidations

An invalidation removes objects from edge caches before they expire:

aws cloudfront create-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --paths "/index.html" "/css/*"

The first 1,000 invalidation paths per month are free across the account, and a wildcard path counts as one path (Pay for file invalidation). AWS recommends versioned file names over invalidation for files that change often, because invalidation does not clear copies held in browsers or corporate proxies.

Measuring cache hit ratio

You cannot tune what you do not measure:

  • Cache hit rate in the CloudFront console. It is one of the additional distribution metrics, which you turn on per distribution for a monthly CloudWatch charge per metric. Origin latency is in the same set.
  • Standard logs, where the x-edge-result-type field shows Hit, RefreshHit, Miss or Error for each request. Grouping misses by path shows which behaviors need a smaller cache key or a longer TTL.

A low hit ratio usually comes from a cache key that includes values the response does not depend on, such as all cookies or tracking query strings, or from origins that send no-cache on content that could be cached.

Getting help

9Acts is an AWS Advanced Tier Services Partner and has worked with clients on AWS since 2016. If you want a second pair of eyes on your CloudFront setup or your wider AWS bill, see our cloud optimization service.

All articles

Book a review

Vikram or Narendar, our partners, reply within one business day.

  1. A partner repliesWithin one business day, with a few questions about your setup.
  2. A 30-minute callWith a senior engineer. We scope what you need and check AWS funding eligibility.
  3. A clear proposalWhat we will do, in what order, and what it costs, before any work starts.

We use these details only to reply to you and prepare for a call. See our privacy policy for what we keep and how to have it deleted.