Simplest explanation of the 6 CloudFront Caching expiration (Time to Live/TTL) scenarios with 7 useful tips

cloudfront-cache-expiration-ttl-featured

CloudFront caching policies and the Minimum TTL, Default TTL, and Maximum TTL directives can make creating caching policies for CloudFront very complicated. This post will explain in the simplest way how to think of these settings as well as provide 7 useful tips.

The official AWS Developers Guide for CloudFront topic Manage how long content stays in the cache (expiration) goes into detail how the Minimum TTL, Default TTL, and Maximum TTL change the caching behavior with 10 scenarios based on the Minimum TTL setting. This article simplifies this by eliminating 4 scenarios and explains each scenario specifically on what is cached in CloudFront and what is cached in the browser.


Brief Explanation of the TTL and the Cache-Control header

Cache-Control headers serve to provide client side applications (web browsers) the amount of time to cache the specified resource in seconds with the “max-age” attribute. For example, if your series of webpages use the same header logo, the header logo request should include a Cache-Control header so the browser knows it will not need to re-download that logo as the visitor navigates to other pages.

CloudFront by default will honor your origins Cache-Control headers when specified. How and when CloudFront does this is controlled by the combination of the Minimum TTL, Default TTL, and Maximum TTL directives in your caching policy.

Keep in mind that CloudFront will cache your content based on the Minimum TTL, Default TTL, and Maximum TTL directives, but CloudFront does not add a Cache-Control header if one is not present.

Expires vs Cache-Control headers Explained

For the sake of simplicity the “Expires” header is no longer a header we should worry about in 2026. if your origin specifies a Cache-Control header as well as the Expires header, the Cache-Control header is used by CloudFront and by browsers. The Expires header is a legacy header that Cache-Control replaced in 1997 as part of the HTTP/1.1 protocol. For nearly 30 years documentation has encouraged developers to use Cache-Control headers instead of Expires headers, yet we still use the Expires header as a fallback if the client does not support Cache-Control. Web browsers today are more likely to support the Cache-Control header than JavaScript. For this reason, it is safe to not worry about the Expires header. If you still are concerned, for the sake of the logic found on this page you can assume that an Expires header will take the place of a Cache-Control “max-age” value and the same rules discussed here will apply.

Cache-Control max-age attribute Explained

The max-age attribute in the Cache-Control header is the number of seconds that the asset should be cached in caching mechanisms. The HTTP specification does not say what the maximum value can be, but AWS CloudFront caching has an absolute maximum of 1 year, which is 31536000 in seconds.

Cache-Control s-maxage attribute Explained

The s-maxage attribute is a special attribute value that can be added to the Cache-Control header to tell caching servers how long they should cache an asset. This allows you to set specific caching values in CloudFront while the default max-age will only apply in browsers.

In most cases this special attribute is not utilized by developers as it should be implemented with the origin service. This can serve a variety of purposes however, two examples are provided.

Example 1, caching the value in CloudFront cache for 1 hour while visitors cache the value for 1 minute. This may be useful if you want the visitor to pull a result on a frequency but get the same result so as long as the result did not change. You can then use the invalidation to clear the cache if the value from the origin changes. The 1 hour could change to 24 hours, but you can see how this would alleviate load to the origin but continue to provide the ability to get the latest information every minute.

Example 2, caching the value in CloudFront cache for 10 minutes while caching the value in the browser for 24 hours. This may be useful if you want the visitor to continue to get the experience they first found when they visited your website, but allows you to update the site content every 10 minutes from your origin for future visitors to get the latest version. This could be used for a web based game board that has a specific start/end time that is different based on the time of the initial visit.

If you do plan on using s-maxage attribute with your Cache-Control headers, keep in mind that this is the desired cache value for the asset in CloudFront only, the max-age header is simply along for the ride and passed onto the browser.

Cache-Control no-cache header Explained

The use of the Cache-Control header to specify no caching with the “no-cache, no-store, and/or private” values is another factor we must consider. No caching headers are critical in situations and they are covered in the scenarios to be discussed next.

No Cache-Control header Explained

The lack of any Cache-Control header in a browser is a performance factor that should be considered. In this scenario, the browser may cache the asset but will also request the asset with additional page views. If the browser sends includes “If-Modified-Since” and/or “Etag” headers, the origin and/or CloudFront may return the entire asset or return an empty request with a special 304 not modified status. This is less efficient than providing Cache-Control headers but provides a way for the browser to preserve bandwidth usage and improve page load performance.

Caching Policies are applied to Behaviors (not the entire distribution)

Caching policies are applied to each behavior (path you specify) you create in CloudFront, giving you flexibility to separate caching and non-caching policies by organizing your application or applications by path on your website. For example your authentication pages /auth/* would be ideally setup with caching disabled, Default (/*) for your public pages you will most likely want caching optimized for compression, /assets/* setup with caching and compression for your css, js, and svg files, and your assets in /assets/images/* setup with caching enabled without compression.

Caching only applies to GET and HEAD requests

Caching with CloudFront only applies to GET and HEAD requests. This means that POST requests (submitting a form) and advanced API operations such as PUT and DELETE are never cached. If you are working with requests that include other methods than GET and HEAD then you will not need to worry about caching policies, the CachingDisabled managed policy will most likely serve you well.


3 Scenarios when Minimum TTL = 0

When the Minimum TTL is set to 0, we rely on the origin to set the Cache-Control headers when caching settings should be applied and leaving them absent when they should not. This is the easiest way to setup caching with CloudFront and the most clear as this is also how the web browser will experience your Cache-Control headers.

Origin sets no Cache-Control header (Minimum TTL = 0)

CloudFront will cache the asset in its cache for the Default TTL value. CloudFront does not add a Cache-Control header to the request, which means the browser will decide on its own how to cache the request.

Origin sets a Cache-Control “max-age” value (Minimum TTL = 0)

CloudFront will cache the asset using the “max-age” value, unless it is greater than Maximum TTL then the maximum value is used to stay within the desired TTL range. The Cache-Control header will continue to be included to the response to the browser, letting the browser cache the asset for the origin specified “max-age”.

Origin sets a Cache-Control header with no-cache value (Minimum TTL = 0)

CloudFront will not cache the request and will allow the Cache-Control header to continue to be included with the response to the browser.


3 Scenarios when Minimum TTL > 0

When the Minimum TTL is set to a value greater than 0, CloudFront will cache assets for at least the Minimum TTL value even if some assets should not be cached in the browser.

Origin sets no Cache-Control header (Minimum TTL > 0)

CloudFront will cache the asset in its cache for the Default TTL, except if the Minimum TTL is greater than the default value, then the Minimum TTL is used. CloudFront does not add a Cache-Control header to the request, which means the browser will decide on its own how to cache the request.

Origin sets a Cache-Control “max-age” value (Minimum TTL > 0)

CloudFront will cache the asset using the “max-age” value, unless it is greater than Maximum TTL or less than Minimum TTL, then the Minimum or Maximum value is used to stay within the desired TTL range. The Cache-Control header will continue to be included to the response to the browser, letting the browser cache the asset for the origin specified “max-age”.

Origin sets a Cache-Control header with no-cache value (Minimum TTL > 0)

CloudFront will cache the request using the Minimum TTL and will allow the Cache-Control header to continue to be included with the response to the browser.

Note: This is a strange scenario, it means that a page from your origin that should not be cached in the web browser is cached in CloudFront. This scenario does have a use case but in most cases this is not a desired result.


6 Useful Tips when creating CloudFront Caching Policies

The following 6 tips are based on my experiences over the years creating and maintaining CloudFront caching policies.

Tip 1: Use the Managed Caching Policies When Possible

AWS provides a number of Managed Caching Policies that work well for most situations. See the CloudFront Managed Cached Policies for details. Here are the most useful.

CachingDisabled managed policy is perfect for behaviors where you never want caching enabled, such as an API.

CachingOptimized managed policy is perfect for scenarios when your files are never going to change for long periods of time and they are not going to be different for different users, meaning cookies, url parameters and headers will not change the assets. This makes this policy ideal for S3 Website origins.

CachingOptimizedForUncompressedObjects managed policy is identical to CachingOptimized managed policy except it adds Gzip and Brotli compression to files. This is ideal for non-compressed files such as svg, css, and js files.

Note that the CachingOptimized and CachingOptimizedForUncompressedObjects use a Minimum TTL of 1. You can essentially think of this as a Minimum TTL of 0 as the item will only be cached momentarily. This is only useful if you have an application that “double taps” the asset in question and your origin does not set the Cache-Control header. So as long as your origin provides the Cache-Control header the Minimum TTL will not apply.

UseOriginCacheControlHeaders managed policy is ideal for web pages themselves that are served from an origin server that will not change based on who is visiting the site, meaning everyone will see the same content.

UseOriginCacheControlHeaders-QueryStrings managed policy is ideal for applications where the query string changes the results found in the content being fetched.

Tip 2: Architect your application with CloudFront behaviors in mind

When you architect your application keep in mind the CloudFront behaviors and the order they will be placed. CloudFront matches requests in the order the behaviors are organized in. Once a path pattern is matched it will take precedence.

Tip 3: Structure your behaviors to minimize the number of behaviors created

When organizing assets (files) of your application, organize them into folders based on the file types. For example, if you have an assets folder, make a separate folder called svg for SVG images. You can also match behaviors based on file extension, e.g. *.svg.

When I am organizing new application, this is how I would structure the application assets for CloudFront behaviors in this order:

  • /assets/
  • /assets/css/
  • /assets/js/
  • /assets/svg/
  • /assets/images/
  • /assets/fonts
  • DEFAULT (/*)

I would then setup behaviors for the following paths:

  • Path: /assets/images/* using the CachingOptimized caching policy.
  • Path: /assets/fonts/* using the CachingOptimized caching policy.
  • Path: /assets/* using the CachingOptimizedForUncompressedObjects caching policy.
  • Path: DEFAULT (/*) using the UseOriginCacheControlHeaders-QueryStrings caching policy, or a custom made policy.

This could also do the following:

  • Path: *.jpg using the CachingOptimized caching policy.
  • Path: *.png using the CachingOptimized caching policy.
  • Path: *.woff2 using the CachingOptimized caching policy.
  • Path: /assets/* using the CachingOptimizedForUncompressedObjects caching policy.
  • Path: DEFAULT (/*) using the UseOriginCacheControlHeaders-QueryStrings caching policy, or a custom made policy.

You could also just use CachingOptimizedForUncompressedObjects, but CloudFront will be impacted as CloudFront may cache a compressed version of your already compressed file.

Tip 4: Make sure your origin includes Cache-Control headers when possible

If your origin does not provide the Cache-Control headers, CloudFront will cache a copy of the URL using the Minimum TTL, if it is greater than 0. More than likely that will not be desired.

Tip 5: DO NOT Add Cache-Control headers with a Response Headers Policy

It is possible, if your origin does not set Cache-Control headers, to add the Cache-Control header in the Response policy. Such a policy option will override the Cache-Control header if it is present from your origin server and would lead to unintended caching behavior, specifically if the origin intended for no caching to occur. As tempting as it may be, DO NOT do this. Instead update your origin to include the appropriate Cache-Control headers.

Tip 6: Set the Cache-Control meta value for assets saved in S3

When storing assets (files) to S3, make sure you set the cache-control meta property. This option is important enough that with the aws cli it has its own argument option –cache-control. The value is the entire value of the Cache-Control header.

aws s3 sync local-folder/ s3://your-bucket-name --cache-control max-age=86400

If you plan on the caching in CloudFront to be different than the caching in the web browser or use the private/public options, specify the value in quotes.

aws s3 sync local-folder/ s3://your-bucket-name --cache-control "public, max-age=60, s-maxage=3600"

Tip 7: If in doubt, always set the Minimum TTL = 0

Setting the Minimum TTL to 0 simplifies a lot, it means you are solely relying on the Cache-Control headers from your origin. This makes things simple, you only have to be concerned with 3 scenarios.


Simplest explanation of the 6 CloudFront Caching expiration scenarios with 7 tips conclusion

The CloudFront Caching policies and how they relate to the Minimum, Default and Maximum TTL settings can be confusing. Hopefully the information provided helps you with setting up CloudFront to maximize the caching potential of CloudFront!

Please feel free to contact me if you need help or have thoughts on CloudFront caching.

Leave a Reply

Your email address will not be published. Required fields are marked *

Join My FREE Newsletter

Get the latest news and episodes of the Cloud Entrepreneur Podcast and Angelo’s development blog directly in your inbox!