How to Cache Your Middleman Site with Fastly
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
-
Create a Fastly account.
Visit fastly.com and click “Try Fastly Now!” to create an account.
-
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
. -
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. -
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 forwww.example.com
to point toglobal.prod.fastly.net
, and anALIAS
record to pointexample.com
toglobal.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.