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:
From left to right:
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:
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
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:
Let’s see how a CloudFront configuration would look like if we set the following restrictions:
Recommended by LinkedIn
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):
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:
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:
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:
Embed mode:
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:
Conclusion
We have covered a lot of things related to caching today, so let me do a very short recap:
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.