Your Website Is Faster Than You Think — The Browser Just Forgot
A user visits your site from the office, sees the page, and leaves. Five minutes later, they come back. Their browser sends a request, your server sends the same files again — the same images, the same CSS, the same JavaScript. Everything is repetitive. The user sees no change, but your server is wasting bandwidth and the user is waiting.
This is exactly where browser caching comes in. Not just to save costs, but so that the second, third, and tenth page loads are nearly instant for the user.
The problem is that most website administrators configure caching headers incorrectly. Or they don't configure them at all. The result? A site that loads in 2.8 seconds on the first visit and 2.8 seconds on the second visit too. You're making no progress.
In this article, we'll explain exactly which headers you should send, what values to set for each, and where most people go wrong.
Cache-Control: The Only Header You Really Need
The Cache-Control header was introduced in HTTP/1.1 and effectively replaced older headers like Expires and Pragma. If you only fix one header, make it this one.
Its simplest form is:
Cache-Control: public, max-age=86400
This means: you can keep this file in cache for 86400 seconds (one day). public means anyone can cache it — the user's browser, proxy, CDN. This value is correct for static files like images, CSS, and JS.
The Difference Between max-age and s-maxage — Where Everything Falls Apart
This is where most guides don't go deep enough. max-age tells the browser how long to cache. s-maxage tells intermediate servers (proxies and CDNs) how long to cache. These two can be different.
Real-world example: You have an HTML page that updates every hour. The user's browser can cache it for 3600 seconds, but your CDN shouldn't hold it for more than 600 seconds because your site's traffic passes through multiple servers and needs to refresh faster.
Cache-Control: public, max-age=3600, s-maxage=600
Here's where people go wrong: Many think s-maxage is an advanced option they don't need. Then they put a CDN in front of their site and notice changes aren't being applied. Users see the old version of the page, and you think the CDN is broken. No, you just didn't set s-maxage.
Another tip: If you set s-maxage, definitely also set max-age. Some browsers, when they see s-maxage, ignore max-age and use the default value. The result? A much shorter cache than you intended.
immutable: One Word That Eliminates Extra Requests
When the browser has cached a file and max-age hasn't expired, it sends no request to the server. But when max-age expires, what happens?
The browser sends a conditional request: "I have this file, has it changed?" If the server says "no," the browser uses the cache. This is a full round trip — usually 50 to 200 milliseconds per file.
For files with versioned names (like style.abc123.css), this round trip is completely unnecessary. Because if the file changes, its name changes, and the browser will send a new request anyway. So why check at all?
Cache-Control: public, max-age=31536000, immutable
One year of caching, and tell the browser "don't even check." This is the best case for static files with versioned names.
But here's where people go wrong: They put immutable on files that don't have versioned names. For example, they cache style.css with immutable and then change it. Users will see the old version for up to a year. immutable is only for files whose names include a hash or version. If you don't do this, don't use immutable.
ETag: The Smart Change Checker
ETag is a unique identifier for a file's content. When the browser sends a conditional request, it sends the If-None-Match header with the previous ETag value. The server compares it, and if nothing has changed, it responds with a 304 Not Modified status — no body, no bandwidth.
It's enabled by default in Nginx. It's also usually enabled in Apache. But if your site is on a shared server and your host has disabled it, you need to check.
To enable it in Apache:
FileETag MTime Size
This means the ETag is built based on the file's modification time and size. Simple and effective.
In Nginx, no configuration is usually needed, but if you want to make sure:
etag on;
One important note: If you have multiple servers (for example, a load balancer with two servers), Apache's default ETag also includes the file's inode. This value differs between servers and causes the browser to re-download the file every time. The solution: Set FileETag MTime Size and remove the inode.
Versioned Names: The Only Correct Way for Static Files
If your file is style.css and you change it, browsers that have cached it will get the old version. Unless you set max-age very short — which means caching is almost useless.
The standard solution: Change the file name. style.abc123.css or style.v2.css. Every time the content changes, the name changes. The browser sees the file as a new resource and downloads it. The old file stays in cache until it expires, but nobody uses it.
Modern tools like Webpack and Vite do this automatically. If you're using WordPress, plugins like WP Rocket do this. If you're doing it manually, just follow a simple rule: every time you change a file, change its name too.
Here's where people go wrong: They change the file name but don't update the links inside the HTML. Or worse, they use a CDN that serves the old cache. Always update the HTML page after changing a file name and purge the CDN cache.
Recommended Values for Each File Type
| File Type | Cache-Control | Explanation |
|---|---|---|
| HTML | no-cache or max-age=0 | Always check, but return 304 if nothing changed |
| CSS/JS with versioned names | public, max-age=31536000, immutable | One year, no check |
| CSS/JS without versioned names | public, max-age=3600 | One hour, then check |
| Images | public, max-age=86400 | One day, usually sufficient |
| Fonts | public, max-age=31536000, immutable | Fonts rarely change |
Note: no-cache doesn't mean "don't cache." It means "check before using." This is an important difference. If you really want something not to be cached, you need no-store — which is only recommended for sensitive data like banking information.
How to Configure Headers on Your Hosting
On Apache, in the .htaccess file:
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|jpg|jpeg|png|gif|svg|woff2)$">
Header set Cache-Control "public, max-age=86400"
</FilesMatch>
</IfModule>
On Nginx, in the location block:
location ~* \.(css|js|jpg|jpeg|png|gif|svg|woff2)$ {
expires 1d;
add_header Cache-Control "public, max-age=86400";
}
If you're on ServerNet Linux hosting, you can use the file manager panel and edit .htaccess directly. If you have SSH access, it's even easier.
After configuring, be sure to check with tools like free webmaster tools that the headers are being sent correctly. A common mistake: You configure the header, but the mod_headers module isn't enabled in Apache. The result? Nothing happens, and you think your configuration is correct.
A Real-World Scenario: What Happens When Everything Is Correct
Let's say you have a page with 50 static files. Without caching, every visit sends 50 requests to the server. With proper caching, the first visit sends 50 requests, the second visit sends 50 conditional requests (with 304s), and the third visit sends 0 requests.
The real numbers: With proper caching, the second page load time is usually 50 to 70% less than the first. If the first load is 3 seconds, the second should be under 1 second. If this isn't happening, either caching isn't configured or something is wrong.
For troubleshooting, use the Network tab in your browser's DevTools. Look at the Size column. If you see 304 Not Modified, caching is working. If you see 200 OK with full size, caching isn't working.
If your site is still slow and caching is correct, the problem is elsewhere. Read the complete guide to diagnosing a slow website — from DNS to database.
Frequently Asked Questions
What's the difference between browser cache and server cache?
Browser cache is stored on the user's device and only works for that specific user. Server cache (like Varnish or Redis) is on the server and works for all users. These two complement each other, not replace each other.
Browser cache reduces server bandwidth and increases speed for returning users. Server cache reduces database load and increases speed for everyone. If you can only configure one, choose browser cache — it's simpler and more effective.
Why are files still being downloaded despite setting Cache-Control?
There are three common reasons. First: You've set the header on the wrong file — for example, on HTML instead of CSS. Second: The browser is in Incognito mode, which disables caching. Third: Another header like Pragma: no-cache or Cache-Control: no-store is being sent from elsewhere and overriding your setting.
Use the browser's DevTools, Network tab, and inspect the response headers. See exactly what Cache-Control value is being sent. If the value isn't correct, the problem is your configuration, not the browser.
Is ETag necessary for all files?
For static files that are cached with versioned names and immutable, ETag is useless — because the browser doesn't check at all. But for HTML files with no-cache, ETag is essential so the browser can determine whether the content has changed or not.
If you disable ETag, browsers typically use the last modification date (Last-Modified). This also works, but it's less accurate — especially if the file changes within the same second.
How much can browser caching improve website speed?
For users who have previously opened the site, typically a 50 to 70% reduction in load time. For new users, it has no effect. That's why browser caching is combined with server caching — one for new users, one for returning users.
If your site has a lot of content and users return frequently, browser caching is your biggest quick win. A few lines of configuration, with no hardware costs.
Comments 0
No comments yet — be the first!