Caching content efficiently around the world with CloudFront
Generated with Adobe Express

Caching content efficiently around the world with CloudFront

Introduction

There are many aspects that influence the loading time of a web application and caching is one of the most important. If I were to give caching a definition, I would define it as a process of determining, storing and delivering relevant content to users of an application, based on variables such as the geographic location of users, deployment frequency of the application and many others that can be domain specific.

In this article I am going to talk about controlling cache in web applications, the different types of cache depending on where the resource is stored, and I’ll describe how AWS’s CloudFront CDN takes advantage of cache directives to deliver content to users around the world.

Introduction to Cache-Control and caching directives

The following diagram describes the components of a very simple web application, the types of cache they represent, and how caching is controlled at every level. I have included components and terminology specific to AWS CloudFront because this is what I'll be focusing on in the second part of the article:

Article content
components of a very simple web application

From left to right:

  • the Resource Origin is the component responsible for creating and providing the requested resource. For instance, a NodeJS server returning the index.html

  • a CloudFront Regional Cache sits in Amazon’s infrastructure, retrieves the resource from the origin and stores it in a region around the world. This is a public cache

  • a CloudFront Point of Presence communicates with the regional cache to bring the content closer to the user. It also caches the resource in its own storage, and it is also a public cache

  • the browser communicates with the point of presence to retrieve the resource and stores it in its own storage on the computer. This is what we call a private cache

Public caches are also referred to as shared caches.

The life span of the resource in each cache is controlled with an HTTP header called Cache-Control and we see it passed from one component to another. It consists of a list of comma-separated directives that can influence different aspects. For instance, our example uses the s-maxage directive to specify that the resource might be stored for 600 seconds in the public cache while the max-age directive tells the browser to store the resource in its private cache for 60 seconds.

When the time specified with these directives passes, the resource is considered stale and is subject to being evicted from the cache.

There are more directives you can implement for your application, some of them can be specified in the response and some can be received in the request, and I invite you to discover them here. While there, take a look at the Age header as well.

For now, there are two specific directives that I find interesting, and I’d like to mention and use in examples going further:

  • stale-while-revalidate is a numeric value expressing the time in seconds a cache can serve a stale resource, measured from the expiration moment, while fetching a newer version from the upstream server, in the background. Thus, preparing the cache for the next user request

  • stale-if-error is a numeric value expressing the time in seconds a cache can serve a stale resource, measured from the expiration moment, in case the upstream server generates an error or when the error is generated locally. When the time is exceeded, the user will receive the error instead of a stale resource.

Now that I’ve introduced the Cache-Control header together with a series of directives, let’s analyze an example response:

Cache-Control: s-maxage=604800 , max-age=604800, stale-if-error=86400         

  • the resource can be considered fresh for a week both in the public and private cache

  • after a week, the cache can return the stale resource for one more day, should the upstream generate an error while trying to compute the newer version of the resource

Article content

Boosting your application with CloudFront

Now that we understand the flexibility of Cache-Control directives, let's see where CloudFront comes into play and why.

The answer is easy: CloudFront is a CDN and acts as a shared cache sitting in front of your application’s servers, storing the resources at the edge in the regional caches and points of presence deployed in Amazon’s infrastructure around the world. We're talking about 34 launched regions and over 600 points of presence, so they definitely cover a lot more ground than we would cover with a pool of servers managed by us.

There are many other features CloudFront provides, some of them enhance security, and I invite you to discover them in the dedicated documentation.

Remember the Meowsee application I introduced in my previous article I wrote on managing configuration? This was the the architecture I designed back then:

Article content
the architecture of Meowsee

Let’s see how a CloudFront configuration would look like if we set the following restrictions:

  1. responses of requests made to the /static/* endpoint can be cached for an indefinite period of time because those files are content addressable (a content addressable file is a file that has a unique name that is usually generated with the content hash at application build time)
  2. responses of requests made to the /kitten/* endpoint can be cached for one week because they display the profile of kittens, and kittens grow faster
  3. when loaded in embed mode, in another website, the responses for requests made to the /kitten/* endpoint can be cached for one month. These requests are differentiated by a query parameter embed=true
  4. every other response can be cached for up to one month

First, from the diagram it results that our CloudFront distribution uses two different origins - an origin is the configuration needed to connect to the service that is able to handle a request and generate a response that is then stored in the cache (you can find more about origins here):

  • for the other origin we’re using the hostname, meowsee.cat, as a origin domain when creating the distribution origin

Article content
create an origin

We have our origins in place, and we are ready to configure the different cache behaviors according to the requirements described above.

A cache behavior allows developers to specify how CloudFront handles requests that match a specific path in your application. Read more about them in AWS’s guide on Cache behavior settings.

First, we’re configuring a behavior for all requests matching the /static/* path to look for the path in the S3 bucket. This behavior uses a predefined cache policy, CachingOptimized, which tells CloudFront to store the results in the cache for up to one year:

Article content
create a the /static cache behavior

When hitting this cache behavior we can expect for the following Cache-Control header to be returned from CloudFront:

Cache-Control: max-age=31536000, s-maxage=31536000

Second, for the requests matching the /kitten/* path we are configuring a new cache behavior. This behavior is a little special because:

  • it uses a custom cache policy to tell CloudFront to include the embed query parameter in the cache key and to cache the response for one week. Custom headers and cookies can be included in the cache key if required as well

Article content
create a cache policy

The interesting thing is that CloudFront has an algorithm which allows overriding the expiration time configured in the cache policy by including the Cache-Control header in the origin’s response. Read here how you can specify the amount of time an object is stored in the CloudFront cache.

Since requests made to /kitten/* are forwarded to our custom origin, we are returning a s-maxage=2592000 directive if the embed query parameter is sent with the request:

Non-embed mode:

  • res: Cache-Control: s-maxage=604800, max-age=604800

Embed mode:

  • res: Cache-Control: s-maxage=2592000, max-age=2592000

In the end, any other request will be mapped to a default cache behavior which matches any (*) path that was not matched by one of the previous behaviors. Needless to say, we will create a new cache policy to store the response for up to one month.

One important takeaway, in my opinion, is that the order of the cache behaviors we define will influence how CloudFront will act on various requests. This has a direct influence in the way traffic is managed between the components I shared in the beginning: the browser, the CDN and the origin it connects to. In other words, things that require a more aggressive caching strategy need to have a higher priority in the list and to be more restrictive and/or specific.

Before wrapping this up, let me show you how to figure out if CloudFront returned a cache response or not. For this you should check for a x-cache header in the response. When set to “Hit from cloudfront” or “RefreshHit from cloudfront” it means you’ve received content from the cache:

Article content
cached content

Conclusion

We have covered a lot of things related to caching today, so let me do a very short recap:

  • I started by introducing the Cache-Control header and a set of directives that can be useful when building a web application

  • then I moved forward to CloudFront and described how it leverages the Cache-Control headers to allow developers to specify different caching behaviors depending on their application entry point

I hope you will find CloudFront as interesting and useful as I did, and that you can find a way to use its global distribution of points of presence around the world to cache and deliver content your content as fast as possible to the users of your application. 

To view or add a comment, sign in

More articles by Marius Ioan Sofron

Insights from the community

Others also viewed

Explore topics