How to Cache Your Middleman Site with Fastly

Anton Zolotov •

Learn how to make your static site serve 40 Million requests per day with minimal effort.

Speed is always a good investment as long as you avoid premature optimization. A faster site is a better user experience, especially on mobile devices. It also improves your search rankings, as search engines factor load time into where you’re placed in the results.

In just 30 minutes of work, I improved our average response time 27X and doubled the number of pages we can serve.

Site Architecture

gunpowderlabs.com is a static site built with Middleman. This allows us to use many of the tools we use in our Rails projects, such as ERB, Markdown, Rack Middleware, and the Asset Pipeline.

A static site doesn’t have a database or content management system. It’s delivered to the user exactly as it’s stored on the server, which simplifies development and hosting.

We deployed the site to Heroku, where we host almost all our projects.

Benchmarking

I established a baseline performance with Blitz. Blitz is a load testing tool. It sends hundreds of concurrent virtual users to your site, so that you can identify bottlenecks before launch.

I used Blitz to run a “rush” — a 60 second load test. It starts by sending one user from four different regions to the specified URL and increases the amount over the duration of the test until each region is hitting the site with 250 users. We chose California, Japan, Australia, and Singapore for our test.

Results

Our original deployment served 6,288 requests in 60 seconds. 2,341 requests timed out, and the average response time was 449 ms. That’s an estimated 23 million requests per day. Not bad for a simple setup, but the timeouts are a bad sign, and I wanted to see how much better we can make it.

Issues With Hosting on a Single Server

There are two issues with hosting on a single server in a single datacenter like we’re doing with Heroku:

Latency

The first problem is that Heroku hosts our site in a specific geographic region, such as Virginia.

When someone in Virginia looks at the site, it gets to them quickly, because the signal has to travel a very short distance.

For someone in Australia, the request needs to travel from their device in Australia all the way to Virginia, and then the response has to go back all the way to Australia. Even at the speed of light, this takes significantly longer.

Server load

The other problem is that our Heroku instances need to handle all the requests, and there’s a limit to how many it can do in parallel, even for a simple static site. We could spin up more dynos or use a local cache, but that would not alleviate the latency issue.

Fastly to the Rescue

The solution to both issues is to reduce the distance between all visitors and the site. The way to do this is through a Content Delivery Network (short “CDN”). A CDN is a set of distributed servers that can store a local copy of the site, so that the closest server delivers the site to each user.

Rather than hitting our server directly, all requests go through Fastly first. If Fastly has a cached version on hand, it serves that. If it doesn’t have a local version, it passes the request through to our server, receives the response, and stores it for the next visitor.

Cache Invalidation

There’s a small wrinkle: What happens when we publish a new blog post, and someone visits the blog? She’d get an old version of the page, without the latest blog post. We need to somehow tell Fastly to delete all the stored versions when we update a page. That’s called Cache Invalidation.

Invalidating caches is really hard if you want to invalidate only the ones that changed. We avoided this issue by purging the entire cache, as our site is updated infrequently and has relatively few pages.

Fastly handles all this beautifully. Let’s take a look at how to set it up.

Setting up Fastly to Cache our Site

  1. Create a Fastly account.

    Visit fastly.com and click “Try Fastly Now!” to create an account.

  2. Configure your site

    Give it any name you want.

    Under Server Address, fill in the address that Heroku gives you, not your full domain.

    Under Domain Name, enter your site’s domain, like example.com.

  3. Test your service

    On the next screen, Fastly gives you a URL like http://example.com.global.prod.fastly.net. Visit this URL and you should see your site.

  4. Set up your DNS

    In your DNS provider (we use DNSimple), you need to point your domain at Fastly instead of Heroku. Create a CNAME record for www.example.com to point to global.prod.fastly.net, and an ALIAS record to point example.com to global.prod.fastly.net as well.

Now, whenever you visit example.com you should still see your site.

Purge the Cache on Deploy

We want to purge the entire cache each time we deploy a new version to Heroku. We can do this through their API.

Our current Rakefile looks like this:

namespace :assets do
  task :precompile do
    sh "middleman build"
  end
end

First, we add a simple object with a method to purge the entire cache (you’ll have to add gem "rest-client", require: false to your Gemfile):

# Purge cache after precompiling assets
class Fastly
  def self.purge_all
    require 'rest_client'

    fastly_service_id = ENV['WWW_FASTLY_SERVICE_ID']
    fastly_api_key = ENV['WWW_FASTLY_API_KEY']

    puts "Busting Fastly Cache..."

    request_headers = {
      content_type: :json,
      'X-Fastly-Key' => fastly_api_key
    }
    response = RestClient.post "https://api.fastly.com/service/#{fastly_service_id}/purge_all", nil, request_headers

    if response.code == 200
      puts "Fastly cache busted!"
    else
      puts "Uh oh: #{response.code}: #{response.body}"
    end
  end
end

Then, we need to make sure that it’s purged on every deploy:

namespace :assets do
  task :precompile do
    sh "middleman build"
    # This line is new
    Fastly.purge_all
  end
end

That’s it!

Set the API keys

Grab your Service ID and API Key from the site and add them to Heroku:

heroku config:set WWW_FASTLY_SERVICE_ID=your_fastly_service_id
heroku config:set WWW_FASTLY_API_KEY=your_fastly_api_key

Deploy

Next time you deploy to Heroku the rake task will purge the cache after middleman build.

Making Sure Fastly Works

Run the following curl command and look at the headers

$ curl -I www.example.com

The headers should have an X-Cache: MISS.

Run it again and you should see a X-Cache: HIT.

Limitations

This approach works well technical teams that can write Markdown and update the site a few times a day at most.

For larger sites (thousands of pages), purging everything might be inefficient, and you should only invalidate the pages that changed.

Conclusion

By putting Fastly’s CDN in front of our Middleman site, we improved our response time from an average of 449 ms to 16 ms, and eliminated timeouts altogether.

Our visitors benefit because the pages load quickly, and we benefit from potentially better search rankings and not having to think about the performance of our site for the foreseeable future.


If you enjoyed this post, please Subscribe via RSS and follow us on Twitter.