HOW TO: Set Cache Control Headers in Nginx or Apache

March 28, 2019

Do you want to set some cache control headers for static assets (like images) on your site the right way to get some solid browser cacheability going on your site? Here's the copy/pasta:

The Nginx Method

location ~* ^.+\.(ico|css|js|gif|jpe?g|png|woff|woff2|eot|svg|ttf|webp)$ {
    expires 30d;
    add_header Pragma public;
    add_header Cache-Control "public, no-cache";

You'll want to add any extensions you want to include within the ( ) and also want to tinker with the expiry time. You can see above expires 30d will request the browser caches the resource for 30 days, but you may want it longer.

You may also be wondering about the no-cache directive. "But wait! I want to cache my resources. Why am I setting using no-cache?", I hear you ask.

So let's say you make a change to an image on your site that's already cached by your visitor's browsers from a previous pageview. They won't see that for 30 days from the first day it was cached.

A lot of folks have suggested that using no-cache is a way to actually disable browser caching on those resources which wouldn't be great - because you want caching for best performance So they suggest using must-revalidate instead of no-cache.

But they're wrong.

Well, not wrong. It will work. But, the must-revalidate directive in cache control headers tells the browser that each of the resources must be rechecked and if the version on the server differs from the cached version, it's freshly served to the browser.

However, in the nasty case that your server doesn't respond to a revalidation request while using must-revalidate in your header (which is common if your web server is dealing with high traffic), the browser or proxy will return a 504 errors for those requests to your visitors. This can either manifest as those resources not loading, or as a full page 504 - depending on how your site is coded.

It's somewhat misleading, but no-cache doesn't actually "uncache" the resources like many people think.

What the no-cache value in the cache control header actually does is request revalidation from the server (using an 80 bytes package) for the resource before serving it from the browser cache. If it hasn't changed since the cached version, the browser will serve the cached version like with must-revalidate, but the difference in this case is your visitors don't see any 504s - just the same old cached resource.

It means you still get to utilise browser caching, but also have updates to resources push out when they're changed without the user having to clear their history or use incognito mode to see your modified content.

What does that even mean?

To summarise, when using no-cache in the cache control header, the browser will just show the cached content if it gets a bad response, but also update if it gets a successful one. There are times to use must-revalidate of course, but this should be considered carefully with an experienced developer.

Often the time to use must-revalidate will simply be to get better test results for cacheability because they don't factor the above. If that's what you're after, say no more...

The other Nginx Method (that gets better test results)

location ~* ^.+\.(ico|css|js|gif|jpe?g|png|woff|woff2|eot|svg|ttf|webp)$ {
    expires 30d;
    add_header Pragma public;
    add_header Cache-Control "public, must-validate";

Apache (in .htaccess)

Often you won't have access to modify Nginx configuration, though most web hosting providers will still add the above code to your site on request. Still, if you want a little more control over it you can also accomplish the same without modifying Nginx at all. You can add the following to your .htaccess file in the root of your instead:

<FilesMatch "\.(ico|css|js|gif|jpe?g|png|woff|woff2|eot|svg|ttf|webp)$">
Header set Cache-Control "max-age=604800, public, no-cache"

The subtle difference here is that the age is set in seconds with max-age=604800. 604,800 seconds is 30 days. But you can change that to whatever you like.

Here's a handy breakdown to save you the math:

  • 1 hour: max-age=3600
  • 1 day: max-age=86400
  • 1 week: max-age=604800
  • 1 month: max-age=2628000
  • 1 year: max-age=31536000

As with the nginx bit above, you can swap out the no-cache value with must-revalidate if you're trying to get high scores on a page performance test like GTMetrix or Google Pagespeed Insights.