Home Play Pinja Bobbity flop

A blog about Ruby, Rails and other Tech. Mostly.

Back to blog

17th Oct 2007, 8:01pm
Adding an Expires header with apache for Rails

We have a problem - we really do. Each time a user requests a page they have to make 50 http requests just to get back a "Not modified" message from the web server. Their browser is asking about every little image and css file and so on. Every one on the page. Those messages are small, but they add up. And as we know, "make fewer HTTP requests" is Steve Souder's number one rule for speeding up your website.

What we need to do is to add an Expires header to our static content - one that gives a time a long way in the future so that the browser knows that the content should stays in the cache -and not be fetched again, or even checked (if it has been updated) from the server. But what if we want to change a css file or a js file? The user will get the old one from their own cache. No good.

Well Rails has a mechanism to prevent this - it adds a 10 digit number on the end of each url (as generated by image_tag, stylesheet_link_tag, or javascript_include_tag for instance) in the query string. That number is based on the file modification time - so when the file is updated then the URL will change. It's like having a remote way to expire the item in the browser's cache.

So it's simple right? Just turn on mod_expires in Apache? Not so fast. What about those images and items that do not have the query string? We don't want to send an expires header for those. Definitely not. (And you will likely have some - for instance images that are referenced from your css files.) If you do, the user is stuck with their cached version even if you change the file on the server.

So we need some way of selectively turning on the expires header in apache.

One way [Danny Burkes] (look at the update at the bottom) is to segregate by directory what you want to expire and what not. But this seems a bit clumsy to manage. (Also, side issue, I am not totally convinced that the munging of the urls provided by the plugin is needed - at least section 13.9 of the HTTP 1.1 spec seems to suggest that content with query strings will be cached fine if an explicit expires header is given.)

Actually you can do what you need with one symbolic link and some apache magic. The obvious magic won't work - you can't detect what's in a query string using a LocationMatch or FilesMatch container. But we can get around this with a rewrite rule, and a directory container. This is from my apache httpd.conf file:

  # add something we can do a directory match on
  RewriteCond %{QUERY_STRING} ^[0-9]{10}$
  RewriteRule ^(.*)$ /add_expires_header%{REQUEST_URI} [QSA]

  # the add_expires_header directory is just a symlink to public
  <Directory "/path/to/rails_app/public/add_expires_header">
    ExpiresActive On
    ExpiresDefault "access plus 10 years"
This detects those query strings (we assume you don't use 10 digit query strings for anything else), and adds a directory on the front of the path.

We use this as something we can detect in a Directory container. And in there we turn on the expires header.

We need one more thing:

cd /path/to/rails_app/public
ln -s . add_expires_header
The symbolic link doesn't go anywhere, and that's just what we want. All the images and css files and whatnot will be found in their usual places. It's pretty unobtrusive -you don't need to change anything in your app to start to benefit from the expires header.

It's a big benefit - we have literally gone from 50 HTTP requests per page to about 16. And with some tweaking we'll get it down more - some of those are from references to images in css, but a few are due to us not using urls generated by rails for static content. And we can fix those.

Back to blog