How to add Expires Headers in WordPress? An Easy Guide
Some links on this page are affiliate links. If you buy through one, WPNeon earns a commission at no extra cost to you. It does not change what we recommend — see how we review.
By the end of this guide you will have a block of server configuration that tells browsers to keep your images, fonts, CSS and JavaScript for up to a year instead of downloading them again on every visit. Allow about ten minutes on Apache and five on Nginx. It clears the “add expires headers” and “efficient cache policy” warnings that speed tools raise, and it makes repeat visits noticeably quicker.
One thing to be clear about before you start: expires headers are set by your web server, not by WordPress. You cannot add them from a child theme’s functions.php or a snippets plugin, because WordPress never runs when a browser asks for a .jpg or a .css file. The server answers those requests on its own. So the work happens in your .htaccess file on Apache, or in your site’s server block on Nginx.
What an expires header actually does
The first time somebody opens your site, their browser downloads every file the page needs: the logo, the stylesheet, the scripts, the photographs. An expires header attaches a “keep this until” date to each of those files.
On the next visit the browser checks that date. If the file has not expired it uses its own stored copy and makes no request at all. Fewer requests means a faster page, less work for your server and a smaller bandwidth bill.
Those files are served straight from disk, so caching them in the browser does not save any database queries. What it saves is round trips over the network, which is usually the slower part.
The rule of thumb is simple. Give long lifetimes to things that rarely change, such as your logo, icons and fonts. Give shorter ones to files you edit often. Never give a long lifetime to your HTML, or visitors will keep seeing yesterday’s page.
Also read:
– W3 Total Cache Settings Explained
– WordPress Image Optimizer Plugins Compared
Where speed tools report this
Run your site through GTmetrix, Pingdom or PageSpeed Insights and you will usually see this flagged either as “add expires headers” or, in newer Lighthouse-based reports, as an inefficient cache policy for static assets.

The warning is the same problem under two names, and the fix below clears both.
Before you edit anything
Download a copy of the file you are about to change. A single stray character in .htaccess can take an Apache site down with a 500 error, and having the original on your desktop turns that from a crisis into a thirty second fix.
If your host gives you a staging site, make the change there first.
Adding expires headers on Apache
Log in to cPanel and open the File Manager.

Navigate to the public_html folder, or whichever folder holds your WordPress installation.

Your .htaccess file lives there.

If you cannot see it, the File Manager is hiding dotfiles. Click Settings in the top right, tick “Show Hidden Files (dotfiles)” and save.

Right click the file and choose Edit.

Paste the following block into .htaccess, either above the # BEGIN WordPress line or below # END WordPress. Do not paste it between those two markers, because WordPress rewrites everything inside them and your rules would eventually be wiped.
<IfModule mod_expires.c>
ExpiresActive On
# Anything not listed below
ExpiresDefault "access plus 1 week"
# HTML: always check for a fresh copy
ExpiresByType text/html "access plus 0 seconds"
# Images
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/avif "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType image/x-icon "access plus 1 year"
# Video
ExpiresByType video/mp4 "access plus 1 year"
ExpiresByType video/webm "access plus 1 year"
# Fonts
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
# CSS and JavaScript
ExpiresByType text/css "access plus 1 month"
ExpiresByType text/javascript "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
# Feeds and documents
ExpiresByType application/rss+xml "access plus 1 hour"
ExpiresByType application/pdf "access plus 1 month"
</IfModule>
Save the file and reload your site. If you get a 500 error, restore the copy you downloaded.
What each part is doing
ExpiresActive On switches the module on. ExpiresByType sets a lifetime for one MIME type, and ExpiresDefault catches everything you did not list. All three are provided by Apache’s mod_expires module and all three are permitted in .htaccess, provided your host allows the Indexes override, which shared hosts almost always do.
The <IfModule mod_expires.c> wrapper is your safety net. If the module is not compiled into your server, Apache skips the block instead of throwing an error.
The readable date format is "access plus 1 year", where the base can be access, now or modification and the unit can be years, months, weeks, days, hours, minutes or seconds. Change the numbers to suit your site. A year suits images and fonts because you rarely change them in place. A month suits CSS and JavaScript because you might.
Setting HTML to zero seconds is deliberate. It lets the browser cache the page but forces it to check with your server before reusing it, so people always see your latest posts.
Adding expires headers on Nginx
Nginx has no .htaccess file. The rules go in the server block for your site, usually somewhere under /etc/nginx/sites-available/ or /etc/nginx/conf.d/. You need SSH or root access, so on managed hosting you may have to ask support to apply it.
Add this inside your server { ... } block:
location ~* \.(?:css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff|woff2|mp4|webm)$ {
expires 1y;
add_header Cache-Control "public";
}
Then test the configuration and reload:
sudo nginx -t
sudo systemctl reload nginx
The expires directive is valid in http, server and location blocks, and it writes both the Expires and the Cache-Control header for you. The extra add_header Cache-Control "public" line is optional. It is worth knowing that add_header directives are only inherited from an outer block when the current block has none of its own, so if you already use add_header elsewhere in the same location, list all of them together.
Check that it worked
The quickest test is a single request from your terminal. Point it at any image on your site:
curl -I https://example.com/wp-content/uploads/2020/03/expires-headers-1.png
You are looking for two lines in the response:
Cache-Control: max-age=31536000
Expires: Fri, 04 Sep 2026 12:00:00 GMT
A max-age of 31536000 is one year in seconds, so that response is cached correctly. If you see no Cache-Control line at all, the rules are not being applied: check that you edited the right file, and on Apache that mod_expires is enabled. You can also read the same headers in your browser’s developer tools under the Network tab.
Or let a caching plugin do it
If you do not want to touch server configuration, a caching plugin will write these rules for you.
WP Rocket does it on activation with no settings to configure. Its documentation states that it “will set expiration lengths on certain types of files and will modify your .htaccess file with these rules”, using a year for static assets, four months for media, a week for the favicon, an hour for feeds and zero seconds for HTML. That is the same pattern as the block above. On Nginx it cannot write the rules for you, so you still add them by hand.
Licences run at $59.95 a year for one site, $119.95 for three and $299.95 for fifty, with a fourteen day refund window. Prices checked September 2026.
W3 Total Cache is the free route and handles browser caching in its Browser Cache panel. If you already run either plugin, check whether the headers are set before adding anything by hand, because two sets of rules for the same file types will only confuse you later.
There is also a dedicated plugin on the WordPress repository, now called AEH Speed Optimization, which writes the rules from a settings screen. It is still maintained, but it has a small user base and it now bundles minification, lazy loading and image optimisation alongside the caching rules. For most sites either a proper caching plugin or the two blocks above is the cleaner choice.
Get WP Rocket and skip the config files
The short version
Expires headers are a five minute change with a permanent payoff. On Apache, paste the mod_expires block into .htaccess outside the WordPress markers. On Nginx, add the location block and reload. Then confirm with curl -I that you are seeing a Cache-Control header on your images.
Keep HTML at zero seconds, static assets at a year, and revisit the numbers only if you start editing CSS several times a day. If you would rather not edit config files, a caching plugin covers it.
Next, the two changes that usually matter more than caching headers: compressing your images properly and deciding whether you still need a lazy load plugin now that browsers handle it natively.
@
Hi there, I use LiteSpeed webserver. Is there any way I can add the expire headers in it? Because the plugin you mentioned above, I installed it on my website but it was giving me an error that it is only for Apache server. So, I wanted to know if you or someone reading this post knows if it is possible to add the expire header in the Litespeed web server.
@
This is a really good explanation! Thank you so much! It saved me a lot of time.