---
title: A Guide to Caching Your Rails Application With Fastly
teaser: Understand and implement Fastly caching in your Rails application.
tags: web,performance,rails
author: Jessie Young
published_on: 2014-12-05
---

In this blog post, I will explain how [Fastly](http://www.fastly.com/) 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](http://www.fastly.com/blog/caching-dynamic-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.

The `Surrogate-Key` HTTP header values tell Fastly which pieces of dynamic
content are on a particular page. Continuing with the blog example, you might
have a `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](http://en.wikipedia.org/wiki/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."](http://techliberation.com/2008/12/18/some-basics-about-edge-caching-network-management-net-neutrality/)
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

### DNS Setup

After you've signed up for Fastly, you'll need to do a little configuration to
get it going:

1. 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.

   ![Fastly host configuration](https://images.thoughtbot.com/fastly-hosts.png)

1. Set the domain.

   Set the domain to whatever you want your site's domain to be.  In my case, it
   was `www.jessieayoung.com`

   ![Fastly domain configuration][fastly-domains]

1. 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](https://dnsimple.com), which is what I use to manage my domains.

1. 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
   and my `www` subdomain, which is pointing to the Fastly <abbr title="Uniform
   Resource Locator">URL</abbr> above:

[fastly-domains]: https://images.thoughtbot.com/fastly-domains.png

```bash
% curl --silent --verbose --output dev/null jessieayoung.com

< HTTP/1.1 200 OK

...

< X-Rack-Cache: miss
```

vs:

```bash
% 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
  that `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 `fastly-rails` gem.

### Rails setup

Install the [`fastly-rails`](https://github.com/fastly/fastly-rails) gem:

```ruby
# Gemfile
gem "fastly-rails"
```

```bash
% bundle
```

Add the Fastly environment variables to your `.env` file:

```ruby
# .env
FASTLY_API_KEY=replace_me
FASTLY_SERVICE_ID=replace_me
```

And add the Fastly configuration file:

```ruby
# 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 <abbr title="Application Programming
Interface">API</abbr> key and Service ID][fastly-account-info].

[fastly-account-info]: https://docs.fastly.com/guides/account-management-and-security/finding-and-managing-your-account-info

### Setting the correct headers

Now that you have your environment variables for Fastly, you are ready to set
the `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.

The `fastly-rails` gem provides a convenience method of
[`set_cache_control_headers`](https://github.com/fastly/fastly-rails#headers)
that sets the `Surrogate-Key` header. Set a `before_action` of
`set_cache_control_headers` on every GET controller action you would like to
cache.

```ruby
# 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 `set_surrogate_key_header`
method to tell Fastly what the values of `Surrogate-Key` should be for that
page:

```ruby
# 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
```

The `fastly-rails` gem adds the `record_key` method to any instance of an
`ActiveRecord::Base` class. For a `post` with an `id` of 1, `post.record_key`
would return `posts/1`. It also adds the `table_key` method to every
`ActiveRecord::Base` class, which returns the name of the model name. In the
case of `Post`, it returns `posts`.

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:

```ruby
# 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:

```ruby
# 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:

```ruby
# 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

As the `fastly-rails`
[README](https://github.com/fastly/fastly-rails/blob/master/README.md) explains,

> "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."

The [`set_surrogate_key_header`
method](https://github.com/fastly/fastly-rails/blob/master/lib/fastly-rails/action_controller/surrogate_key_headers.rb#L7)
removes the `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 `Set-Cookie`
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:

```ruby
# config/application.rb
config.middleware.delete "ExampleMiddleware"
```

and ran `curl` on my site again. When I confirmed that the `Set-Cookie` header
was no longer present, I knew which piece of middleware to override. To
re-delete the `Set-Cookie` header, I placed the `fastly-rails` middleware
directly before my authentication middleware:

```ruby
config.middleware.insert_before(
  ExampleMiddleware,
  "FastlyRails::Rack::RemoveSetCookieHeader"
)
```

With that, `Set-Cookie` was no longer being set and I had Fastly caching up and
running.

## Confirming that Fastly is working

To confirm that Fastly is working, run `curl` on your site a few times and confirm
that the `X-Cache` header is returning a `HIT`. Once it is, you're good to go!

## Credits

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](http://www.hward.com/) for helping me solve the `Set-Cookie` issue and
being an-all around awesome dude.

## Further reading

* [Varnish Cache Invalidation with Fastly Surrogate
  Keys](http://www.hward.com/varnish-cache-invalidation-with-fastly-surrogate-keys)
* [Scale Rails with Varnish HTTP Caching
  Layer](http://www.hward.com/scale-rails-with-varnish-http-caching-layer)
* [API Caching](http://www.fastly.com/blog/api-caching-part-1)
* [Surrogate Keys](http://www.fastly.com/blog/surrogate-keys-part-1)
