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"  </Directory>

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/publicln -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.


4 thoughts on “Adding an Expires header with apache for Rails

  1. Thanks! Stumbled upon your blog by searching for how I can change logging level in Rails, but just kept reading until I got to this article.Never actually though about this issue. I definitely need to implement this kind of expiration mechanism on my systems..cheers,Mike.

  2. […] can find out there that can leverage that to really speed things up.?? I ended up with something along these lines.?? It does require a symlink that gets wiped out by our Capistrano deploy (so it needs to recreate […]

  3. Hello, I’m doing some freshing up on speed optimizing and came to your and Steve’s website.. Already familiar with compression of source codes which in returns gives a 20-40 increase depending on sizes. Just trying to learn more each day…do you have a news letter? Oh I also have stumbled your page <a href="; title="cms website hosting service provider" rel="nofollow">here</a>..

  4. […] playing with a wide variety of approaches, we settled on conditionally adding an expires header. ??That’s been in our app for more than a year now and we’ve been pretty pleased with […]

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s