In this blog post, I will explain how Fastly works and how to set up Fastly in a Rails application. I will also give some troubleshooting tips based on my experiences implementing Fastly in a Rails application.
Understanding CDNs and cache invalidation
Fastly is a content delivery network (CDN). A CDN is a system of distributed servers that deliver web content to a user based on geographic location.
Giant Robots (this blog) uses Fastly. If you are reading this post from Australia, the content you are reading was likely retrieved from a different web server than the content that my coworkers here in San Francisco are reading.
You and my coworkers are reading the same content, just delivered from different locations to accelerate load time. But what if the blog post has a typo and I push up a new version? How will these distributed servers know that the content they hold has changed?
The answer to these questions is: cache invalidation. But what does that really mean?
Caching, most generally, refers to any way a computer stores something so that it can be quickly accessed later on. Browsers, for example, cache web pages by storing a copy of the pages you visit and then use that copy when you re-visit rather than retrieving all of the data from the web server again.
Cache invalidation is the process by which you tell the services that cache your content that they are no longer up to date and need to retrieve updated content rather than deliver the outdated copy that they hold. Cache invalidation is important because without it we could not cache dynamic web content.
Understanding Fastly caching
The simplest way to do cache invalidation is to “purge” the entire cache each time a change is made in your application. This means that you tell service that is caching your site - in this case, Fastly - to retrieve all new content because the cache is no longer a valid representation of your site. This can work nicely for a relatively static site, such as a blog, that is only updated once a day. But what if your blog has comments and gets a new comment every minute?
This is where Fastly really shines. As opposed to requiring you to purge the entire cache for every change to your web application, Fastly uses what they call “surrogate keys” to dynamically determine which pages of your application need cache invalidation and which remain the same.
Surrogate-Key HTTP header values tell Fastly which pieces of dynamic
content are on a particular page. Continuing with the blog example, you might
Surrogate-Key value of
posts on the home page and then a value of
posts/1 on the page that just shows your first post. When you update the first
post, you will want Fastly to purge the home page (because it shows the first
post) and the page that shows just the first post, but not the rest of the
blog post pages, because those were unchanged.
Think of the
Surrogate-Key header as a map that tells Fastly where each item
in your system appears. When that item changes, Fastly goes to each location on
the map and refreshes the content so that it is up to date.
While you can purge the entire cache for a site that uses Fastly, page-specific cache invalidation means that you are not purging the cache for pages that remain the same. Preserving the cache for most pages means that Fastly only needs to hit your web servers when necessary.
A note about edge caching
Confusingly, Fastly’s documentation uses the terms “CDN” and “edge caching” interchangeably. Edge caching is a specialized form of edge computing, an umbrella term that refers to pushing “applications, data and computing power (services) away from centralized points to the logical extremes of a network”.
Edge caching is a form of caching that uses de-centralized servers “to move content ‘closer’ to the end-users that view it to avoid the latency that occurs as packets traverse longer distances across the network.” Fastly provides an edge caching service through its CDN.
Now that you have a basic understanding of how Fastly works, let’s dive into implementing Fastly in a Rails application.
Getting started with Fastly
After you’ve signed up for Fastly, you’ll need to do a little configuration to get it going:
- Set the host.
In my case, I am using Heroku, so my address is a Heroku hostname. The “Address” field is cut off, but it is the same value as the “Name” field.
- Set the domain.
Set the domain to whatever you want your site’s domain to be. In my case, it
Set the CNAME for your site to the Fastly endpoint. In my case it was
www.jessieayoung.com.global.prod.fastly.net. I did this via DNSimple, which is what I use to manage my domains.
Confirm that your DNS setup is working.
Once the DNS changes propagate, you should see some new headers when you
curl the site you are caching.
Compare the headers of the apex domain, which I have not set up to use Fastly
www subdomain, which is pointing to the Fastly URL above:
% curl --silent --verbose --output dev/null jessieayoung.com < HTTP/1.1 200 OK ... < X-Rack-Cache: miss
% curl --silent --verbose --output dev/null www.jessieayoung.com < HTTP/1.1 200 OK ... < X-Served-By: cache-lax1426-LAX < X-Cache: MISS < X-Cache-Hits: 0 < X-Timer: S1415387356.979543,VS0,VE1212
These new headers are being set by Fastly. I am getting a
MISS, which means
curl is hitting my web server rather than the cached version of
the site. This is because I haven’t added Fastly to my Rails app. Next step:
setting up the
# Gemfile gem "fastly-rails"
Add the Fastly environment variables to your
# .env FASTLY_API_KEY=replace_me FASTLY_SERVICE_ID=replace_me
And add the Fastly configuration file:
# config/initializers/fastly.rb FastlyRails.configure do |config| config.api_key = ENV.fetch("FASTLY_API_KEY") thirty_days_in_seconds = 2592000 config.max_age = thirty_days_in_seconds config.service_id = ENV.fetch("FASTLY_SERVICE_ID") end
Make sure you set these environment variables in your application’s staging and/or production environments. Fastly’s documentation contains detailed information on where to find your Fastly API key and Service ID.
Setting the correct headers
Now that you have your environment variables for Fastly, you are ready to set
Surrogate-Key header on the pages of your Rails app that you want to cache
and purge. As the section on understanding Fastly caching above explains, Fastly
relies on the values of the
Surrogate-Key header to determine which pages to
purge when data in your application changes.
fastly-rails gem provides a convenience method of
that sets the
Surrogate-Key header. Set a
set_cache_control_headers on every GET controller action you would like to
# app/controllers/posts_controller.rb class PostsController < ApplicationController before_action :set_cache_control_headers, only: [:index, :show]
Now, in the controller methods, you can use the
method to tell Fastly what the values of
Surrogate-Key should be for that
# app/controllers/posts_controller.rb ... def index @posts = Post.all set_surrogate_key_header Post.table_key, @posts.map(&:record_key) end def show @post = Post.find(params[:id]) set_surrogate_key_header @post.record_key end
fastly-rails gem adds the
record_key method to any instance of an
ActiveRecord::Base class. For a
post with an
id of 1,
posts/1. It also adds the
table_key method to every
ActiveRecord::Base class, which returns the name of the model name. In the
Post, it returns
Passing an array of surrogate keys to
set_surrogate_key_header tells Fastly
which objects are included on that page so that when it comes time to purge,
Fastly will know which pages display your object and thus need to be purged.
Selectively purging records
Setting the surrogate keys, like you did in the previous section, tells Fastly which objects are included on each page. Now, you need to tell Fastly when to purge the pages for each object. This can be done in the controller or via callbacks.
Including these purges in the controller will look like this:
# app/controllers/posts_controller.rb def create @post = Post.new(post_params) if @post.save @post.purge_all render @post end end def update @post = Post.find(params[:id]) if @post.update(params) @post.purge render @post end end def delete @post = Post.find(params[:id]) if @post.destroy @post.purge @post.purge_all end end end
Including these purges in the controller is ideal. If your application data is not hitting a controller during creation / deletion / update (eg: you are updating via Rails console or using an engine like RailsAdmin), you can instead include these methods as callbacks:
# lib/active_record_extension.rb if Rails.env.staging? || Rails.env.production? module ActiveRecordExtension extend ActiveSupport::Concern included do after_create :purge_all after_save :purge after_destroy :purge, :purge_all end ActiveRecord::Base.send(:include, ActiveRecordExtension) end end
Creating a Rake task to purge all
While the setup above takes care of purging individual records, you will also want to purge your entire Fastly cache after each deploy of your application. This is easy to do via the Fastly web UI, but it’s even easier to do with this small Rake task:
# lib/tasks/purge_fastly_cache.rake namespace :fastly do desc "Purge Fastly cache (takes about 5s)" task :purge do require Rails.root.join("config/initializers/fastly") FastlyRails.client.get_service(ENV.fetch("FASTLY_SERVICE_ID")).purge_all puts "Cache purged" end end
Debugging tip: Fastly does not cache HTTP responses with cookies
“By default, Fastly will not cache any response containing a Set-Cookie header. In general, this is beneficial because caching responses that contain sensitive data is typically not done on shared caches.”
Set-Cookie header. In some cases, you may
curl and find that the
header is still there. After a thorough investigation, I found that my
authentication library was adding
Set-Cookie back in after it was removed by
fastly-rails. Because Fastly does not cache responses with a
header, this meant that none of my pages were being cached.
To debug this behavior, I ran the handy
rake middleware task, which outputs
the middleware stack for the application. In the output, I saw the name of my
authentication library’s middleware. To test whether this middleware was the
culprit, I temporarily added the following:
# config/application.rb config.middleware.delete "ExampleMiddleware"
curl on my site again. When I confirmed that the
was no longer present, I knew which piece of middleware to override. To
Set-Cookie header, I placed the
directly before my authentication middleware:
config.middleware.insert_before( ExampleMiddleware, "FastlyRails::Rack::RemoveSetCookieHeader" )
Set-Cookie was no longer being set and I had Fastly caching up and
Confirming that Fastly is working
To confirm that Fastly is working, run
curl on your site a few times and confirm
X-Cache header is returning a
HIT. Once it is, you’re good to go!
Big thank you to the Fastly team for all of their help in getting my project set
up and working properly. And a second big thank you to
Harlow for helping me solve the
Set-Cookie issue and
being an-all around awesome dude.