How to Enable mod_expires for Caching in Apache

How to enable mod_expires for caching in Apache

There’s a specific moment I remember from a site audit a while back — running a PageSpeed test and seeing “Leverage browser caching” flagged as a major issue, with dozens of static assets being re-downloaded on every single page view. The fix took about ten minutes: enabling mod_expires. That’s genuinely one of the best time-to-impact ratios in web performance work, and I set it up on every site I manage now, as a matter of habit.

Here’s exactly how I configure it.

What Is mod_expires

mod_expires is an Apache module that controls the Expires and Cache-Control: max-age HTTP headers, telling browsers (and any caching proxies or CDNs in between) how long they’re allowed to reuse a cached copy of a resource before checking back with the server.

Without these headers, browsers have to guess how long to cache something, and that guess is often way too conservative — leading to unnecessary repeat downloads of assets like logos, stylesheets, and scripts that rarely change.

Prerequisites

  • Apache installed and running
  • Root or sudo access
  • Comfort editing Apache config or .htaccess files

Step 1: Enable mod_expires

On Debian/Ubuntu:

sudo a2enmod expires
sudo systemctl restart apache2

On CentOS/RHEL, mod_expires is typically built into the base install — just confirm it’s loaded:

apachectl -M | grep expires

If it’s missing, ensure the module is loaded in your httpd.conf or conf.modules.d/:

LoadModule expires_module modules/mod_expires.so

Step 2: Basic Configuration

I usually put this in a virtual host block or a dedicated config file. Here’s the setup I use on most sites:

<IfModule mod_expires.c>
    ExpiresActive On

    # Default expiration
    ExpiresDefault "access plus 1 month"

    # Images
    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 image/x-icon "access plus 1 year"

    # CSS and JavaScript
    ExpiresByType text/css "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
    ExpiresByType text/javascript "access plus 1 month"

    # Fonts
    ExpiresByType font/woff "access plus 1 year"
    ExpiresByType font/woff2 "access plus 1 year"
    ExpiresByType application/vnd.ms-fontobject "access plus 1 year"
    ExpiresByType font/ttf "access plus 1 year"

    # HTML (short cache, since content changes)
    ExpiresByType text/html "access plus 0 seconds"

    # Data formats
    ExpiresByType application/json "access plus 0 seconds"
    ExpiresByType application/xml "access plus 0 seconds"
</IfModule>

Understanding ExpiresActive, ExpiresDefault, and ExpiresByType

  • ExpiresActive On — turns the module on for the current scope; without this, none of the other directives do anything.
  • ExpiresDefault — sets a fallback expiration for any file type not explicitly listed.
  • ExpiresByType — sets expiration for a specific MIME type, overriding the default for that type.

The Expiration Syntax

The expiration string follows the pattern <base> plus <number> <unit>:

ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType text/css "modification plus 1 week"
  • access — measured from when the client last accessed the file
  • modification — measured from when the file was last modified on the server

I use access almost exclusively — it’s simpler to reason about and works well for typical static asset caching.

Why HTML Gets Special Treatment

Notice that I set text/html to access plus 0 seconds. This is intentional: HTML pages tend to change (content updates, new posts, price changes), so I don’t want browsers holding onto stale copies. Static assets like images, CSS, and JS, on the other hand, rarely change in place — and when they do, I use cache-busting filenames (like style.a1b2c3.css) rather than relying on cache expiration to catch the update.

Per-Directory Configuration

Sometimes I only want aggressive caching applied to a specific assets folder:

<Directory /var/www/example.com/public_html/assets>
    <IfModule mod_expires.c>
        ExpiresActive On
        ExpiresDefault "access plus 1 year"
    </IfModule>
</Directory>

Using .htaccess (When You Don’t Have Access to Main Config)

On shared hosting, or when I don’t have access to the main Apache config, I set this up in .htaccess instead:

<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType text/css "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
</IfModule>

Note that AllowOverride for the relevant directory needs to include Indexes or All for this to take effect in .htaccess.

Testing and Reloading

sudo apachectl configtest
sudo systemctl reload apache2   # Debian/Ubuntu
sudo systemctl reload httpd     # CentOS/RHEL

Verifying Caching Headers

curl -I https://example.com/assets/logo.png

Look for:

Cache-Control: max-age=31536000
Expires: Mon, 26 Jul 2027 12:00:00 GMT

I also check this in browser dev tools under the Network tab — the “Size” column will show “(disk cache)” or “(memory cache)” for repeat requests to properly cached assets.

Combining with mod_headers for Cache-Control

mod_expires sets the Expires header and a basic Cache-Control: max-age, but I often pair it with mod_headers for more explicit control, especially around public/private and immutable directives:

<IfModule mod_headers.c>
    <FilesMatch "\.(jpg|jpeg|png|webp|gif|svg|css|js|woff2?)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
</IfModule>

The immutable directive tells supporting browsers not to even revalidate the file until the max-age expires — useful for assets with cache-busting filenames that truly never change once published.

Real-World Use Cases

  • Image-heavy sites: Photography portfolios, e-commerce product pages — huge repeat-visit speed gains.
  • Sites with a CDN in front: Proper Expires/Cache-Control headers tell the CDN how long to hold onto cached copies too.
  • Single-page apps: Long-cache the built JS/CSS bundles (with hashed filenames), short-cache the index.html.
  • Documentation sites: Static assets rarely change; long cache lifetimes meaningfully reduce repeat bandwidth.

Troubleshooting Tips

  • Headers not appearing at all: Confirm the module is loaded (apachectl -M | grep expires) and that ExpiresActive On is actually in scope for the request path.
  • Users seeing stale content after a deploy: This usually means static assets were cached too aggressively without a cache-busting filename strategy — always version your asset filenames when using long expiration times.
  • .htaccess rules not applying: Check AllowOverride settings for that directory in the main config.
  • CDN not respecting your cache headers: Some CDNs override origin cache headers with their own settings — check your CDN’s cache configuration separately.

Common Mistakes to Avoid

  1. Long-caching HTML pages — leads to visitors seeing outdated content after updates.
  2. Setting a long cache time on assets without a cache-busting strategy — you’ll be stuck waiting out the cache duration to push urgent fixes.
  3. Forgetting to test with curl -I and assuming headers are set correctly without verifying.
  4. Confusing access vs modification base — modification can behave unexpectedly if file timestamps aren’t reliable (e.g., after a fresh deploy that touches all files).
  5. Not pairing with mod_headers when more explicit Cache-Control behavior (like immutable or no-store for sensitive pages) is needed.

Security Best Practices

  • Never long-cache pages containing sensitive or personalized data — use Cache-Control: no-store for things like account pages or checkout flows.
  • Be cautious caching API responses that include user-specific or sensitive data; scope caching rules narrowly to genuinely static, public content.
  • Review cache headers on any page behind authentication to make sure private data isn’t inadvertently cached by shared/public caches.

Performance Optimization

  • Combine mod_expires with mod_deflate/mod_brotli compression for maximum speed benefit — smaller files, cached longer.
  • Use long cache lifetimes (a year) for genuinely immutable assets, paired with hashed/versioned filenames.
  • Use short or zero cache lifetimes for frequently changing content like HTML and API responses.
  • Pair with a CDN for further performance gains — origin caching headers control how long the CDN itself holds content too.

Frequently Asked Questions

Q: What’s the difference between Expires and Cache-Control? A: Expires is an older HTTP/1.0-era header with an absolute date; Cache-Control: max-age is the modern HTTP/1.1 equivalent using a relative time. mod_expires sets both for broad compatibility.

Q: How long should I cache my static assets? A: For truly static assets with cache-busting filenames, a year is standard. For content that might change without a filename change, stick to a much shorter window (hours to a few weeks).

Q: Will enabling mod_expires break anything on my site? A: It shouldn’t, as long as you avoid long-caching dynamic content like HTML pages or API responses without a versioning strategy.

Q: Do I need mod_expires if I’m using a CDN? A: Yes — most CDNs respect (or at least reference) your origin’s cache headers to determine their own caching behavior, so it’s still worth configuring properly at the Apache level.

Q: Can I set different cache times for different folders? A: Yes, wrap your ExpiresByType/ExpiresDefault directives inside a <Directory> block scoped to that folder.

Summary and Key Takeaways

Enabling mod_expires is a small config change with an outsized impact on real-world page speed for repeat visitors. To recap:

  • Enable the module and set sensible defaults with ExpiresDefault and ExpiresByType.
  • Long-cache truly static assets (images, fonts, versioned CSS/JS); short-cache HTML and dynamic content.
  • Pair with mod_headers for finer-grained Cache-Control behavior.
  • Use cache-busting filenames for any asset with a long cache lifetime.
  • Verify with curl -I and browser dev tools, not just assumption.

It’s one of the easiest performance wins you can implement in a single afternoon.

References

Total
1
Shares

Leave a Reply

Previous Post
How to set up mod_security for Apache

How to Set Up mod_security for Apache

Next Post
How to use mod_alias for URL redirection in Apache

How to Use mod_alias for URL Redirection in Apache

Related Posts