Skip to main content
Home / Articles /

Strategies for Cache-Busting CSS

Major performance gains are to be had from browser caching CSS. You ensure your server is set up to send headers that tell the browser to hang onto the CSS file for a given amount of time. It’s a best-practice that many if not most sites are doing already.

Hand-in-hand with browser caching is cache busting. Say the browser has the CSS file cached for one year (not uncommon). Then you want to change the CSS. You need a strategy for breaking the cache and forcing the browser to download a new copy of the CSS.

Here are some ways.

The CSS has to be cached for this to matter…

Just to make sure, here’s what some healthy looking headers look like for a cached CSS file:

We’re looking for that Cache-Control and Expires header. I’m not a server config expert. I’d probably look at the H5BP server configs. But here’s some kinda classic Apache/HTAccess ways to get that going:

<FilesMatch "\.(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)(\.gz)?$">
<IfModule mod_expires.c>
  ExpiresActive on
  ExpiresByType text/css                  "access plus 1 year"
  ExpiresByType application/javascript    "access plus 1 year"

Query Strings

Most browsers these days will see a URL with a different query string as a different file and download a fresh copy. Most CDN’s even support and recommend this.

<link rel="stylesheet" href="style.css?v=3.4.1">

Make small change? Change it to:

<link rel="stylesheet" href="style.css?v=3.4.2">

You could potentially make it easier on yourself by setting a server side variable to use in multiple places. Thus changing it would break cache on lots of files at once.

<?php $cssVersion = "3.4.2"; ?>

<link rel="stylesheet" href="global.css?v=<?php echo $cssVersion; ?>">

Perhaps you could even use Semantic Versioning. You could also define a constant.

Changing File Name

Query strings didn’t always work. Some browsers didn’t see a differnt query string as a different file. And some software (I’ve heard: Squid) wouldn’t cache files with query string. Steve Souders told us not to.

A similar concept was to change the file name itself. Like this in the HTML:

<link rel="stylesheet" href="style.232124.css">

You would handle this programmatically, not literally change the file name in your project. Since that file doesn’t actually exist on the server, you’ll need to perform some trickery to route it to the right file. Jeremy Keith covered his technique for this fairly recently.

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+).(\d+).(js|css)$ $1.$3 [L]


He uses Twig, so the templates he uses are ultimately like:

{% set cssupdate = '20150310' %}


I’m sure you can imagine a version of that in any backend language (like ASP). Level up by making a build tool or deployment script update the variable itself.

Basing Cache Busting “Number” on File Updated Date

While searching around about this cache busting stuff, you’ll see a lot of advice recommending you use the server to check when the file was last updated to create the cache busting “number” (number, meaning, whatever thing you change to bust cache).

function autoversion($url) {
  $path = pathinfo($url);
<link href="<?php autoversion('/path/to/theme.css'); ?>" rel="stylesheet">

I can’t speak well to this. It seems to me that asking your server to dig up this information on every pageview would be pretty intensive and dangerous in production. In the past I’ve done things like “I’ll just have PHP output the dimensions of the image in data attributes!” only to find it grinds the server to halt. Anyway, beware.


ETags kinda seem like a good idea, because the whole point of them is information to check if the browser already has a copy of that file.

But most advice out there says: “turn off your ETags headers”. Yahoo says:


Another issue is that they just aren’t as effective as actual caching. In order to check an ETag, a network request still needs to be made. It’s not just the downloading of files that affect performance, it’s all the network negotiation and latency stuff too.

Again, not an expert here, but here’s what’s generally recommended to turn them off in Apache land:

<IfModule mod_headers.c>
  Header unset ETag
FileETag None

Framework Does It For Us

Rails Asset Pipeline

I have a little experience with the Rails Asset Pipeline and Sprockets. It’s kind of a dream system if you ask me. I link up stylesheets in templates:

<%= stylesheet_link_tag "about/about" %>

And it produces HTML like:


That cache busting number only changes when the file changes, so you only break cache on the files that need broken. Plus it has methods for images and JavaScript as well.


If you use a page caching tool in WordPress, like W3 Total Cache or something, you probably have to be less afraid of that filemtime business being too server intensive.

Gilbert Pellegrom posted a WordPress-specific technique using it:

wp_enqueue_style( 'screen' );

// Example Output: /style.css?ver=1384432580

The WordPress plugin Busted! does this same thing behind the scenes. Just does it kinda automatically to everything.


CodeKit doesn’t have a built in method for changing file names, but it does have a way to execute Shell scripts under circumstances you set up.

Michael Russell has a blog post about how you can inject timestamps into file themselves, which I’m sure you could modify to change the filenames instead.

Build Tools

All the popular task runner / build tool thingies have plugins to help change file names. Sufian Rhazi has a post on doing it in raw Node.js as well.




Within Preprocessors

When linking up assets within other assets (e.g. an image that you link to from within a LESS file, for example) you could put the preprocessor to work. Ben Nadel has a post on doing just that.

Async CSS

With Critical CSS becoming more of a thing, deferred loading of CSS is becoming more of a thing. There are some other reasons to defer loading of CSS as well (perhaps print CSS, or priming cache).

If you’re loading CSS with loadCSS (or perhaps injecting a link tag), you’ll need to update the file name it requests in the JavaScript itself. Different than changing the file name, but not that different.


Anything I missed? What’s your cache busting strategy?