Bleacher Report is transitioning from a monolithic Rails app towards a service-oriented architecture. In our journey, we’ve established a few guidelines for services. One of those key guidelines is integration with Fastly.

Fastly is a Content Delivery Network (CDN). Their killer feature is caching dynamic content. Fastly uses a system of surrogate keys to accomplish this.

Surrogate keys allow you to selectively purge single or related content. For example, you can purge the surrogate key articles/1/comments without having to purge articles/1. For a RESTful web service, this proves to be tremendously useful.

Caching will reduce the number of requests hitting your origin server, as well as result in quicker response times for both consumer and internal APIs.

This tutorial will show you how to accomplish API caching with fastly-rails.

Setup

Before you can begin, you’ll need to configure a few things. Adding a new service requires that you specify the origin server, as well as the domain.

Next, you’ll need to point the CNAME for api.example.com to api.example.com.global.prod.fastly.net. This may take a moment to propagate.

It’s worth noting that Fastly uses Varnish under the hood. Services can use custom VCLs. Here’s an example:

/* main.vcl */
if (obj.status >= 500 && obj.status < 600) {
 /* deliver stale object if it is available */
 if (stale.exists) {
   return(deliver_stale);
 }
}

One of the most powerful features of a custom VCL is the ability to serve stale content. Fastly has a tutorial available on the setup process, which I strongly suggest you enable.

To confirm that your setup is successful, run the following command:

curl -I -H -svo /dev/null https://api.example.com

You’ll notice that Fastly unique headers are present. Now it’s time to begin caching and purging dynamic content!

Installation

Add fastly-rails to your Gemfile:

gem 'fastly-rails', '~> 0.2.0'

Next, you’ll need to add an initializer:

# config/initializers/fastly_rails.rb
FastlyRails.configure do |config|
 config.api_key = ENV["FASTLY_API_KEY"]
 config.max_age = 2592000 # 30 days in seconds
 config.service_id = ENV["FASTLY_SERVICE_ID"]
end

Caching

To cache using fastly-rails, you’ll need to set the Cache-Control and Surrogate-Key headers:

# app/controllers/v1/tags_controller.rb
class V1::TagsController < ApplicationController
  before_action :set_cache_control_headers, only: [:show, :index]

  def show
    @tag = Tag.find(params[:id])
    set_surrogate_key_header @tag.record_key
    render json: @tag
  end

  def index
    @tags = Tag.all
    set_surrogate_key_header Tag.table_key, @tags.map(&:record_key)
    render json: @tags
  end
end

Cache-Control sets the time to live (TTL). The before_action filter accomplishes this by calling the set_cache_control_headers method before the #show and #index actions.

Surrogate-Key sets the surrogate key by calling the set_surrogate_key_header method with an object’s record_key. For example, tags/1 for a single object and tags for a collection.

Purging

We’ve all heard some variation of the following quote:

“There are only two hard things in Computer Science: cache invalidation, naming things and off-by-one errors.”

Despite it’s age, this quote is still surprisingly true. However, Fastly has managed to make cache invalidation much more pleasant.

There are multiple ways to purge content. Each approach takes advantage of the #purge and #purge_all instance methods. For example, you can purge in your controller:

# app/controllers/v1/tags_controller.rb
class V1::TagsController < ApplicationController
  before_action :set_cache_control_headers, only: [:show, :index]

  def delete
    @tag = Tag.find(params[:id])
    if @tag.destroy
      @tag.purge
      @tag.purge_all
    end
  end
end

It’s worth noting that I’m not a fan of this approach. It can become difficult to maintain and it is not very DRY. In an effort to keep controllers as lean as possible, I would recommend purging using callbacks:

# app/models/tag.rb
class Tag < ActiveRecord::Base
 after_create :purge_all
 after_save :purge
 after_destroy :purge, :purge_all
end

Rails offers before and after callbacks, which makes it much easier to just set it and forget it. In addition to programmatically purging, you can also use the Fastly dashboard. Purging can be as fast as 150 milliseconds, so it doesn’t really matter how you do it.

That’s it. You’ve successfully accomplished API caching with Fastly and Rails. Whether your API is public or private, it should now be much faster!