You ran your site through PageSpeed Insights or GTmetrix, and there it is: "Leverage browser caching" or "Serve static assets with an efficient cache policy." The report even lists your own CSS, JS, and image files as the culprits. Here's what that warning actually means and how to fix it on both cPanel shared hosting and a VPS, without breaking anything.
What "leverage browser caching" actually means
Every time a visitor loads your site, their browser downloads your logo, your theme's CSS, your jQuery bundle, and a dozen other files. If your server doesn't tell the browser how long it's allowed to keep those files, the browser re-downloads them on every single visit — even if the file hasn't changed in months.
The fix is two HTTP response headers: Cache-Control and Expires. They tell the browser "this file is good for 30 days, don't ask me for it again." Nothing to install, no plugin required — it's a few lines of server configuration.
Symptom: where you'll actually see this
- PageSpeed Insights flags "Serve static assets with an efficient cache policy" and lists dozens of your own files with "0 seconds" or "1 hour" TTL
- GTmetrix gives you a low score on the "Leverage browser caching" YSlow rule
- Repeat visitors complain the site "feels slow every time," even though you've already added a caching plugin
- Checking response headers with curl shows no
Cache-ControlorExpiresline at all on static files
That last one is the easiest way to confirm it. Run this against any image or CSS file on your domain:
curl -sI https://yourdomain.com/wp-content/themes/yourtheme/style.cssIf you don't see a Cache-Control or Expires header in the response, that's your problem confirmed.
Cause: your server just isn't sending the headers
A default Apache or Nginx install doesn't set cache lifetimes on static files out of the box — it just serves them with no caching instructions at all, leaving the browser to guess (and browsers guess conservatively, usually re-validating every load). Page-caching plugins like LiteSpeed Cache, WP Rocket, or W3 Total Cache handle the HTML page cache, but most don't touch the response headers for images, fonts, or third-party assets — that's a server-level setting, not a WordPress one. This is why the warning often persists even after you've installed a caching plugin.
Fix: set Cache-Control and Expires headers
On cPanel / Apache (.htaccess)
Open File Manager (or connect over SFTP), edit the .htaccess file in your site's root, and add this block above the WordPress rewrite rules:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType application/pdf "access plus 1 month"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(jpg|jpeg|png|webp|svg|css|js|woff2)$">
Header set Cache-Control "public, max-age=2592000"
</FilesMatch>
</IfModule>
Both mod_expires and mod_headers are enabled by default on Getwebup cPanel accounts. If you get a 500 error right after saving, one of those modules is disabled — open a ticket and we'll check EasyApache.
On Nginx (VPS)
If you're running WordPress or a static site on a VPS behind Nginx, add this inside your server block, not inside a location / that already proxies to PHP-FPM:
location ~* \.(jpg|jpeg|png|webp|svg|gif|css|js|woff2|ttf)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
}Reload Nginx after editing:
sudo nginx -t && sudo systemctl reload nginxnginx -t tests the config before you reload, so a typo doesn't take the site down.
Verify it worked
Re-run the same curl command from earlier. You should now see something like:
cache-control: public, max-age=2592000
expires: Thu, 17 Sep 2026 10:00:00 GMTThen re-test in PageSpeed Insights or GTmetrix — the warning should either disappear or shrink to just third-party scripts (Google Fonts, ad tags, analytics) that you don't control the headers for.
Choosing cache lifetimes
Longer isn't always better — a logo you never change can cache for a year, but a CSS file you tweak weekly shouldn't, or visitors will keep seeing stale styles.
| File type | Suggested lifetime | Why |
|---|---|---|
| Images, fonts, icons | 1 year | Rarely change; when they do, the filename usually changes too |
| CSS / JS (no version hash) | 1 week – 1 month | You need updates to reach visitors reasonably fast |
| CSS / JS (with version hash, e.g. style.css?ver=6.4) | 1 year | The filename changes when the content does, so long caching is safe |
| HTML pages | Don't cache in the browser | Handled by your page-cache plugin or CDN instead, with proper purging |
Common mistakes that undo this fix
- Editing the wrong .htaccess. WordPress installed in a subdirectory has its own .htaccess separate from the domain root — make sure you're editing the one WordPress actually serves from.
- A CDN in front of the site. If you're on Cloudflare, your origin headers matter less once Cloudflare's own edge cache takes over — but Cloudflare respects your origin's Cache-Control by default, so this fix still helps rather than conflicts.
- Caching HTML too aggressively. Don't add long Expires headers to .php or .html files this way — that belongs to your page-cache plugin, which knows how to purge on content changes. A hardcoded year-long Expires header on your homepage means visitors see stale content until they hard-refresh.
- Forgetting mod_headers isn't the same as mod_expires. Some minimal Apache builds have one but not the other. Use the curl check to confirm both are actually taking effect, not just assume the config applied.
Prevention: keep it working after updates
Theme and plugin updates occasionally overwrite or regenerate .htaccess (Yoast SEO and some security plugins do this). After any major update, re-run the curl check on a couple of static files. It takes ten seconds and saves you from silently losing this fix months later when someone asks why the PageSpeed score dropped again.
Browser caching vs. server-side caching — not the same thing
It's easy to assume that once you've installed a page-cache plugin or set up Redis object caching, browser caching is covered too. It isn't. Server-side caching (page cache, object cache, OPcache) speeds up how fast your server builds a page. Browser caching (what this article covers) decides whether the visitor's browser needs to ask your server for a file at all on their next visit. You want both — they solve different halves of the same slow-site problem.