Is your WordPress site failing performance audits like Google PageSpeed Insights or GTmetrix? One of the most common (yet easiest to fix) reasons for a low score is the “Add Expires Headers” warning.

When this is missing, your server fails to tell visitors’ browsers which files (like images, CSS, and fonts) should be saved locally, forcing them to re-download your entire design every single time they visit a new page. This not only destroys your page speed but also eats up your bandwidth and hurts your SEO rankings.

In this guide, we will break down exactly how “Expires Headers” work and why they are essential for a fast, responsive WordPress site. You will learn the differences between Expires and Cache-Control, how to implement these settings on NGINX and Apache servers, and how to troubleshoot common issues such as CDN conflicts or files that won’t update after a deployment.

Let’s get started!

What Are “Expires Headers” and Why Do They Matter?

“Expires Headers” are instructions sent by your web server to a visitor’s browser that define how long the browser should keep a file in its local cache.

When an audit flags this as an issue, it means your server is telling the browser to check for new files too frequently, or to cache them not at all. Resolving this allows the browser to load your design elements directly from the user’s computer, dramatically increasing page speed.

Expires vs. Cache-Control: What’s the Difference?

While they sound similar, they are two different methods for managing how long files stay in a browser’s memory:

  • Cache-Control: This is the modern, preferred standard. It uses “max-age” to define a duration (e.g., “cache this for 30 days”).
  • Expires: This is an older method that requires you to set a specific calendar date and time.

You can use both at the same time. This ensures maximum compatibility; modern browsers will prioritize the newer Cache-Control header, while older browsers will reliably fall back to the Expires header.

Suggested Read: How to Easily Fix the Leverage Browser Caching Warning in WordPress

Caching Reference Table for Novices

Not all files are created equal when it comes to caching. Setting a one-year expiration for your main HTML file would be disastrous, as your visitors would rarely see new content. Conversely, caching an image for only an hour is a massive waste of bandwidth.

Before we dive into the technical steps for adding headers, it’s important to understand which expiration durations are appropriate for different file types. The table below outlines recommended caching lengths for common website assets.

File TypeRecommended DurationWhy?
CSS & JS1 YearThese rarely change; cache busting handles updates.
Images (JPG, PNG)1 YearImages are heavy; caching them saves massive bandwidth.
Fonts (WOFF, TTF)1 YearFonts don’t change and are essential for rendering design.
HTML Files0 to 12 HoursHTML changes often; you want users to see new content quickly.
Favicon1 WeekA small file that rarely changes but is safe to update occasionally.
Third-Party ScriptsN/AYou cannot control external scripts (e.g., Google Analytics).

Suggested Read: Server Cache vs. Browser Cache vs. Site Cache: What’s the Difference?

How to Add Expires Headers in WordPress

Adding Expires headers is a simple way to speed up your site, but it requires modifying your server settings. If you aren’t sure which web server you use (Apache or Nginx), ask your hosting support team before proceeding.

Step 1: Check what your server is sending right now

Before you change anything, see if your site already has these headers.

  1. Open your website in Chrome or Firefox.
  2. Right-click anywhere on the page and select Inspect.
  3. Go to the Network tab at the top of the window that pops up.
  4. Refresh your webpage (F5).
  5. Click on any file in the list (like a .jpg or .css file).
  6. Look for a sub-tab called Headers and scroll down to Response Headers. If you see Cache-Control or Expires, your site is already configured.

Suggested Read: How To Use NGINX FastCGI Cache (RunCache) To Speed Up Your WordPress Performance

Step 2: How to add Expires headers on NGINX for static assets

NGINX handles headers through server configuration blocks rather than a simple file edit, which makes it very fast but slightly more technical to set up.

Note: If you are using RunCloud, you can apply HTTP headers in just a couple of clicks directly through the RunCloud dashboard (no SSH or command-line experience required).

If you are configuring this manually, follow these steps:

  1. Log in to your server: Use an SSH client (like PuTTY on Windows, or the built-in Terminal on macOS/Linux) and connect to your server.
  2. Locate your NGINX configuration file: It is usually found within your site’s NGINX configuration block (located at in /etc/nginx/sites-available/). You will need administrative access to your server to edit these files.
  3. Identify the correct server block: Open the configuration file for your specific domain. Ensure you are editing the file that handles your primary website traffic.
  4. Define the cache duration for file types: Inside your server block, create location rules for the specific file types you want to cache. For example, to cache images for one year, you would add:
location ~* \.(jpg|jpeg|png|gif|ico|svg)$ {
    expires 365d;
    access_log off;
}
  1. Add rules for CSS and JavaScript: In the same way, add a separate block for your static code files to ensure they are also cached for the long term:
location ~* \.(css|js)$ {
    expires 365d;
    access_log off;
}
  1. Test your configuration: Before saving and restarting, always run nginx -t in your terminal to ensure there are no syntax errors that could take your site offline.
  2. Reload NGINX: Once the configuration is verified as valid, reload NGINX using sudo service nginx reload to apply the new headers globally.
how to add expires headers

Step 3: How to add Expires headers on Apache using .htaccess

If your WordPress site is hosted on Apache, you can manage your site’s performance headers by modifying the .htaccess file. Follow these steps to safely update your headers:

  1. Locate the .htaccess file: Log in to your hosting provider’s File Manager (or use an FTP client like FileZilla). Navigate to your WordPress “root” directory (this is the folder that contains your wp-config.php and wp-content folders). If you do not see a file named .htaccess, ensure your File Manager is set to “Show Hidden Files.”
  2. Create the file (if necessary): If you truly do not have an .htaccess file, create a new text file in the root directory and name it exactly .htaccess (ensure there is no .txt extension).
  3. Backup your current file: Before making any changes, right-click your existing .htaccess file and download it to your local computer. If you accidentally make a mistake and your site displays a “500 Internal Server Error,” you can simply upload the original file to instantly restore your site.
  4. Edit the file: Right-click the .htaccess file on your server and select Edit or Code Editor.
  5. Insert the Expires code: Scroll to the very top of the file. Paste the following configuration snippet. 

Important: If you see any existing text that starts with # or looks like a comment (such as the default WordPress rewrite rules), do not delete it; place your new code above or below the existing blocks.

<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresDefault "access plus 1 month"
    
    # Cache static assets for 1 year
    ExpiresByType image/jpg "access plus 1 year"
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/gif "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType text/css "access plus 1 year"
    ExpiresByType text/javascript "access plus 1 year"
    ExpiresByType application/javascript "access plus 1 year"
    ExpiresByType application/x-javascript "access plus 1 year"
    ExpiresByType application/font-woff2 "access plus 1 year"
</IfModule>
  1. Save and Verify: Save your changes and visit your website. Refresh your page a few times. If the site loads normally, your headers are now active. If you see an error page, delete your changes and restore the backup file you created in Step 3.
expires headers in HTTP

Troubleshooting Expire Headers

Even after following the setup steps, you might find that your browser isn’t picking up the changes. This is usually due to an intermediary service or a configuration conflict. Here is how to troubleshoot the most common sticking points.

Expires headers not showing in DevTools or curl

If you added the code but don’t see the headers, your server might not have loaded the new configuration yet.

  • For Apache users, ensure the mod_expires module is actually enabled in your server settings.
  • For NGINX, you must reload the service (e.g., nginx -s reload) for changes to take effect.
  • If you are using a caching plugin, clear its cache entirely, as it may be serving old, cached versions of your pages that do not include the new header instructions.

Suggested Read: NGINX Caching for WordPress – Complete Guide & Tutorial

Cache-Control is overriding Expires 

It is common for these two headers to “compete.” By web standards, Cache-Control (specifically max-age) takes precedence over the Expires date. If your server is configured to set both, but they conflict, the browser will ignore the Expires header entirely. To resolve this, ensure your configuration rules are consistent so that both headers dictate the same expiration duration.

CDN is rewriting or stripping headers

If you use a Content Delivery Network (like Cloudflare or BunnyCDN), the headers you set on your server might be ignored or overwritten by the CDN’s own settings. Check your CDN dashboard’s “Caching” or “Rules” section; it often includes its own “Browser Cache TTL” settings that act as a global override. You may need to adjust the CDN panel settings to match your desired expiration policies.

Suggested Read: Scaling RAM & CPU Cores – How They Affect WordPress Performance

Third-party resources still failing the audit

Tools like PageSpeed Insights will always flag third-party scripts (such as Facebook Pixels or Google Analytics) because you do not have permission to modify headers on external servers. This is expected behavior; you cannot fix it, and it generally does not significantly affect your site’s actual performance score to warrant concern. Focus only on the files hosted on your own domain.

Changes apply, but files do not update after deploy

A long cache expiration value is good for performance, but can lead to deployment headaches. If you update a cached file, such as a CSS stylesheet or JavaScript script, a user’s browser will continue to display the “broken” or outdated version until its expiration date.

This occurs because the browser trusts the long-term header instruction and loads the file from local storage rather than checking the server for an updated copy. As a result, users will not see your latest design or functionality changes.

To overcome this caching conflict without reducing your expiration times, you can use a technique called “cache busting.” You can do this by appending a unique version parameter to the file URL in your theme’s code (e.g., changing style.css to style.css?ver=1.1).

When the file is updated, you simply change this version number. The browser interprets the new URL as a completely different file, forcing it to bypass the old, long-term cache and download your latest changes immediately, ensuring all users see the correct, up-to-date content.

Suggested Read: How To Use Redis Object Cache To Speed Up A Dynamic WordPress Site

Wrapping Up

By correctly setting expiration rules for your static assets, we can ensure that returning visitors experience lightning-fast load times, as their browsers won’t need to re-download elements like CSS, images, and fonts.

To remove the manual guesswork from this process, we highly recommend using RunCloud to effortlessly manage your NGINX configurations.

By centralizing your server management through RunCloud, you can easily apply, reload, and manage your HTTP headers without worrying about complex syntax errors.

For an even smoother experience, pair this with the RunCache plugin to automatically implement the best caching rules for your WordPress site. Together, these tools ensure your site follows modern best practices, allowing you to focus on your content while your server handles the speed optimization automatically.

Start using RunCloud today.

FAQs on Expires Headers

Is the Expires Header the same as the Cache-Control header?

No, they are different methods for controlling browser caching. Cache-Control is the modern standard that uses duration (e.g., “cache for 30 days”), while Expires is an older method that requires a specific calendar date and time.

What is a good Expires value for CSS, JS, images, and fonts?

For these static assets, it is best to set an expiration date for one year in the future. Because these files rarely change, a long duration significantly improves load speeds for returning visitors.

Why do I still see “Add Expires Headers” after setting them?

You may still see this warning because your web server configuration (like NGINX or Apache) is not correctly applying the rules, or your caching plugin needs to be cleared. Additionally, some tools flag the headers even if they are present but set for a shorter duration than their specific performance policy requires.

Can I set Expires headers for Google Fonts or analytics scripts?

You can set headers for Google Fonts if you host them locally on your own server. However, for files hosted on third-party servers, such as Google or analytics providers, you cannot control their headers because those settings are managed exclusively by the external service.

Should I use both Expires and Cache-Control?

Generally, no. Since Cache-Control is the modern and preferred standard, you typically only need to set that one. However, there is no harm in including both Cache-Control and Expires. Modern browsers will use Cache-Control and ignore Expires, but including Expires provides a fallback for very old browsers that may not support Cache-Control.

Will Expires Headers break updates after plugin/theme changes?

They can cause issues because the browser will continue to load the old version of the file until the expiration date passes. To fix this, developers use “versioning” or “cache busting” (adding a query string like style.css?ver=1.1 to the file name) to force the browser to download the updated version immediately.