I didn’t fully appreciate how much caching mattered until I ran a before-and-after load test on one of my sites. Same server, same content, same traffic pattern — but with proper caching enabled, response times dropped dramatically and my server handled roughly three times the concurrent load without breaking a sweat. Caching is one of those “boring” optimizations that quietly does more for performance than almost anything else you can configure.
Understanding the Different Types of Caching in Apache
Before diving into configuration, it’s worth understanding that “caching” in Apache isn’t one single thing — there are several distinct mechanisms, each solving a slightly different problem:
- Browser caching (via
mod_expiresandmod_headers): Tells the visitor’s browser to store assets locally and skip re-requesting them entirely for a set period. - Server-side caching (via
mod_cache,mod_cache_disk, ormod_cache_socache): Apache itself stores responses and serves them directly without regenerating them, saving backend processing time. - Compression (via
mod_deflateormod_brotli): Not strictly caching, but closely related — reduces the size of what needs to be cached and transferred in the first place.
I use all three together for a complete caching strategy.
Prerequisites
- Apache installed with root/sudo access.
- A working website with static and/or dynamic content to test against.
- Basic comfort editing virtual host configuration files.
Step 1: Enable Browser Caching with mod_expires
This is the easiest win and should be step one for virtually every site. Enable the module:
sudo a2enmod expires
Add configuration to your virtual host (or .htaccess if that’s your only option):
<IfModule mod_expires.c>
ExpiresActive On
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"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/x-javascript "access plus 1 month"
ExpiresByType application/pdf "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
Notice I set text/html to expire immediately — HTML pages are often dynamic or change frequently, so I don’t want browsers holding onto stale versions. Static assets like images, fonts, and versioned CSS/JS, on the other hand, can be cached aggressively since I control their filenames and can bust the cache by renaming files when they change.
Step 2: Add Explicit Cache-Control Headers with mod_headers
mod_expires sets Expires headers, but modern browsers (and CDNs) also respect Cache-Control, which offers more granular control:
sudo a2enmod headers
<IfModule mod_headers.c>
<FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|ico|woff2?)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "\.(css|js)$">
Header set Cache-Control "public, max-age=2592000"
</FilesMatch>
<FilesMatch "\.(html|htm)$">
Header set Cache-Control "no-cache, must-revalidate"
</FilesMatch>
</IfModule>
The immutable directive tells browsers this file will never change at this URL — perfect for versioned assets like style.a3f9c2.css where a content change would result in a completely new filename anyway.
Step 3: Enable Compression with mod_deflate
Smaller responses mean less to cache and less to transfer:
sudo a2enmod deflate
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE application/xml application/rss+xml
AddOutputFilterByType DEFLATE image/svg+xml
<IfModule mod_setenvif.c>
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
</IfModule>
</IfModule>
If your Apache build supports it, mod_brotli typically compresses even more efficiently than gzip for text-based assets:
sudo a2enmod brotli
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript
</IfModule>
Step 4: Set Up Server-Side Caching with mod_cache
This is the more advanced layer — Apache actually stores and serves complete responses without regenerating them, which is especially valuable for dynamic content (PHP-generated pages, for example) that doesn’t change on every single request.
Enable the required modules:
sudo a2enmod cache
sudo a2enmod cache_disk
Configure caching in your virtual host:
<IfModule mod_cache.c>
CacheQuickHandler on
<IfModule mod_cache_disk.c>
CacheRoot /var/cache/apache2/mod_cache_disk
CacheEnable disk /
CacheDirLevels 2
CacheDirLength 1
CacheMaxFileSize 5000000
CacheIgnoreHeaders Set-Cookie
CacheDefaultExpire 3600
CacheMaxExpire 86400
</IfModule>
</IfModule>
Make sure the cache directory exists and is writable by Apache:
sudo mkdir -p /var/cache/apache2/mod_cache_disk
sudo chown -R www-data:www-data /var/cache/apache2/mod_cache_disk
Important: CacheIgnoreHeaders Set-Cookie prevents Apache from caching pages containing session cookies verbatim — without this, you risk serving one visitor’s session cookie to another, which is a serious security bug, not just a caching quirk.
Step 5: Excluding Dynamic or Personalized Content from Caching
Not everything should be cached. Logged-in areas, shopping carts, and personalized dashboards need to be explicitly excluded:
CacheDisable /wp-admin
CacheDisable /cart
CacheDisable /account
CacheDisable /checkout
For applications that set cookies to indicate a logged-in state, you can also use CacheIgnoreNoLastMod and conditionally disable caching based on the presence of a specific cookie using mod_rewrite in combination with mod_cache‘s environment variable support.
Step 6: Verify Caching Is Working
Check response headers using curl:
curl -I https://example.com/style.css
Look for Cache-Control, Expires, and (for server-side caching) an X-Cache or Age header confirming the response came from cache rather than being freshly generated.
You can also check the disk cache directly:
sudo find /var/cache/apache2/mod_cache_disk -type f | head
Real-World Use Cases
- Blogs and content sites: Caching rendered HTML pages so PHP/MySQL only runs once per cache period rather than on every single visitor request.
- E-commerce product pages: Caching product listing pages (excluding cart and checkout flows) to handle high traffic during sales events.
- API responses: Caching GET endpoints that return relatively static data, reducing backend database load.
- Static asset delivery: Aggressive browser caching for images, fonts, and versioned CSS/JS across any type of site.
Troubleshooting Common Issues
Problem: Changes to my site don’t show up for visitors. Browser caching is likely holding onto old versions. For versioned assets, change the filename (cache-busting). For HTML, confirm you’ve set no-cache or a very short expiry as I recommended in Step 1.
Problem: One user sees another user’s cached personalized content. This is serious — immediately add CacheDisable rules for any personalized or authenticated routes, and confirm CacheIgnoreHeaders Set-Cookie is in place.
Problem: Server-side cache doesn’t seem to be storing anything. Check directory permissions on your CacheRoot path, and confirm CacheEnable disk / is present and matches the correct virtual host context.
Problem: Compression isn’t being applied. Verify mod_deflate (or mod_brotli) is enabled and that the Content-Type of your response actually matches one of your AddOutputFilterByType rules.
Common Mistakes to Avoid
- Caching authenticated or personalized pages without exclusions, leaking one user’s content to another.
- Setting extremely long cache lifetimes on HTML pages that change frequently, causing visitors to see stale content.
- Forgetting to set proper directory permissions on the disk cache path, silently preventing caching from working at all.
- Not testing after enabling caching — assuming it’s “just working” without verifying headers or cache hit behavior.
- Caching error responses (4xx/5xx) accidentally, which can cause a temporary issue to persist far longer than the actual outage.
Security Best Practices
- Always exclude authentication cookies from cached responses using
CacheIgnoreHeaders Set-Cookie. - Explicitly disable caching for admin panels, checkout flows, and any route handling sensitive data.
- Regularly audit your
CacheDisablerules whenever you add new sensitive routes to your application. - Set appropriate
Cache-Control: privateheaders (rather thanpublic) for any response containing user-specific data that still needs some client-side caching.
Performance Optimization Tips
- Combine all three caching layers (browser, compression, server-side) for maximum benefit rather than relying on just one.
- Use a CDN (see my dedicated CDN configuration post) in front of Apache for an additional caching layer closer to your visitors geographically.
- Monitor your cache hit ratio and adjust
CacheDefaultExpire/CacheMaxExpirebased on how frequently your content actually changes. - For very high-traffic sites, consider pairing Apache’s built-in caching with a dedicated caching layer like Varnish or Redis for object caching at the application level.
Frequently Asked Questions
Is mod_cache the same as a CDN? No — mod_cache caches responses on your own server, reducing backend processing load, while a CDN caches responses at globally distributed edge locations, reducing network latency. They complement each other well.
Will caching break dynamic, personalized content? Only if you don’t explicitly exclude it. Always add CacheDisable rules for authenticated or personalized routes.
How do I clear the Apache disk cache? You can safely delete the contents of your CacheRoot directory; Apache will regenerate cached entries as new requests come in:
sudo rm -rf /var/cache/apache2/mod_cache_disk/*
Should I cache HTML pages? For static or rarely-changing content, yes, with a short-to-moderate expiry. For personalized or frequently-changing content, it’s safer to leave HTML uncached or use very short cache windows with proper revalidation.
Summary and Key Takeaways
Setting up caching in Apache properly requires thinking about it as a layered strategy rather than a single switch to flip. Browser caching, compression, and server-side caching each solve a different part of the performance puzzle.
The key points to remember:
- Use
mod_expiresandmod_headersfor browser-level caching of static assets. - Enable
mod_deflate(ormod_brotli) to reduce response sizes across the board. - Use
mod_cachewithmod_cache_diskfor server-side caching of full responses, especially for dynamic content that doesn’t change on every request. - Always explicitly exclude authenticated, personalized, or sensitive routes from caching.
- Verify your configuration with
curl -Iand monitor cache hit behavior over time.
Done properly, this layered caching approach is one of the most cost-effective performance improvements you can make — no new hardware required, just smarter configuration.