Apache can make a fast site feel slow or a stable deployment feel unpredictable depending on how cache headers are set. This guide gives you a practical, evergreen approach to caching static assets and HTML with Apache so you can improve repeat visits, reduce unnecessary requests, and avoid the common mistake of serving stale content after a release. The goal is not a one-size-fits-all snippet, but a framework you can apply whether you manage a simple VPS, shared hosting with .htaccess, or a more controlled deployment pipeline.
Overview
If you only want the short version, here it is: cache static assets aggressively when their filenames change on deploy, cache HTML conservatively unless you have a deliberate invalidation strategy, and use validators when freshness matters more than maximum cache duration.
In Apache, cache behavior is usually controlled through a mix of:
Cache-Controlresponse headersExpiresheaders, often managed throughmod_expires- Validators like
ETagandLast-Modified - Per-file or per-directory rules set in the main Apache config, virtual host config, or
.htaccess
The core decision is simple but important: different content types deserve different cache policies.
- Versioned static assets such as
app.4f8c1.js,styles.a9b2.css, or fingerprinted images can usually be cached for a long time. - Unversioned static assets should be cached more carefully because browsers may keep using them after you change the file.
- HTML documents usually need short caching or revalidation because they often reference newly deployed assets, updated markup, or user-sensitive state.
That distinction matters more than any particular Apache directive. Once you understand it, the configuration becomes much easier to reason about.
If you are debugging confusing browser behavior, pair this guide with How to Debug Caching Issues in Chrome DevTools. If you want a broader production review, HTTP Caching Checklist for Production Sites is a useful companion.
Core framework
This section gives you a repeatable mental model for Apache cache headers instead of a list of disconnected directives.
1. Start with content categories, not modules
Before writing config, group responses into categories:
- Immutable versioned assets: JavaScript, CSS, fonts, images built with content hashes in the filename
- Semi-stable assets: images or downloads that change occasionally but keep the same URL
- HTML and application responses: documents that should reflect recent changes quickly
- Sensitive or personalized responses: pages behind login, account pages, dashboards, or anything tied to user state
Apache modules help implement the rules, but they should not drive the policy. Your deployment model should.
2. Use long cache lifetimes only when URLs change on deploy
The safest high-performance pattern is:
- Generate versioned filenames during build
- Serve them with long-lived caching
- Treat the filename itself as the cache-busting mechanism
For those files, a header such as Cache-Control: public, max-age=31536000, immutable is often a reasonable target. The exact duration can vary, but the key is that the URL must change when the file changes.
If you cannot guarantee filename versioning, do not set year-long caching on assets that keep the same URL.
3. Keep HTML fresh enough to discover new assets
HTML often acts as the entry point to everything else. If a browser keeps an old HTML document, it may continue requesting old asset URLs even after a new deployment.
That is why many teams use one of these patterns for HTML:
- No cache:
Cache-Control: no-storefor highly dynamic or sensitive pages - Revalidation-first:
Cache-Control: no-cachewith validators, so the browser checks before reuse - Short freshness window: a small
max-agefor pages where brief staleness is acceptable
For public marketing pages or docs, a short cache plus validators can work well. For personalized app shells or authenticated pages, stricter policies are usually safer.
4. Understand what mod_expires and mod_headers each do
In Apache, two common tools are:
mod_expiresfor settingExpiresand cache age by MIME typemod_headersfor directly setting or modifyingCache-Controland related headers
mod_expires is convenient for broad rules such as “images get six months” or “CSS gets one year.” mod_headers is better when you need precise Cache-Control values like immutable, no-store, or must-revalidate.
In practice, many Apache setups use both: mod_expires for baseline durations and mod_headers for exceptions or more explicit policy.
5. Use validators deliberately
Validators help browsers ask, “Has this changed?” without redownloading the full response.
Last-Modifiedis simple and often sufficient for static filesETagcan be useful but may become tricky across multiple servers if generated inconsistently
If you are deciding between them, see ETag vs Last-Modified: Which Validator Should You Use?. If you are trying to understand the impact of browser checks and conditional responses, 304 Not Modified Explained: When Revalidation Helps or Hurts adds important context.
6. Prefer server config over scattered overrides when possible
Apache allows cache rules in the main config, virtual host blocks, directory blocks, and .htaccess. All can work, but central configuration is generally easier to review and keep consistent.
Use .htaccess only when you need per-directory control or when hosting constraints leave no alternative. The real concern is not purity; it is maintainability. If one team member edits cache rules in one place and another adds overrides elsewhere, troubleshooting becomes much harder.
Practical examples
These examples are meant as starting points. Adjust file paths, MIME handling, and deployment assumptions to match your stack.
Example 1: Long-lived caching for versioned static assets
Use this pattern when your build pipeline emits hashed filenames and every deploy changes the asset URL when content changes.
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/javascript "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|png|jpg|jpeg|svg|ico|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule>This is a strong fit for modern front-end build systems. The main risk is using it on files that are not actually versioned.
Example 2: Conservative caching for HTML
If you want browsers to revalidate HTML before reuse, this is a practical baseline:
<IfModule mod_headers.c>
<FilesMatch "\.(html?)$">
Header set Cache-Control "no-cache, must-revalidate"
</FilesMatch>
</IfModule>This does not mean “never store.” It means “check with the server before using the cached copy.” That distinction is easy to miss and is one reason teams accidentally overcorrect with no-store.
If the page contains authenticated or sensitive content, a stricter header may be more appropriate:
Header set Cache-Control "no-store"Apply that narrowly. Using no-store on everything can remove useful browser caching and increase load on origin servers.
Example 3: Moderate caching for unversioned images or downloads
Sometimes you inherit a site where URLs cannot change easily. In that case, choose shorter durations:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/png "access plus 7 days"
ExpiresByType image/jpeg "access plus 7 days"
ExpiresByType application/pdf "access plus 1 day"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(png|jpg|jpeg|pdf)$">
Header set Cache-Control "public, max-age=604800"
</FilesMatch>
</IfModule>This is less efficient than fingerprinted assets, but much safer than pretending stable URLs are immutable.
Example 4: Separate policies by directory
A clean operational pattern is to separate build output and documents physically:
/assets/for hashed static files/or/public/for HTML entry points
<Directory "/var/www/example/assets">
Header set Cache-Control "public, max-age=31536000, immutable"
</Directory>
<FilesMatch "\.(html?)$">
Header set Cache-Control "no-cache, must-revalidate"
</FilesMatch>This tends to age well because the policy follows deployment structure instead of trying to infer intent from every extension.
Example 5: Validate the result, not just the config
After changing Apache rules, verify the response headers with your browser dev tools or a simple command-line request. For example:
curl -I https://example.com/
curl -I https://example.com/assets/app.4f8c1.jsCheck that the actual output matches your intent:
- Does HTML return the expected
Cache-Control? - Do static assets have the long lifetime you intended?
- Are validators present where you expect them?
- Are proxies or CDNs adding or overriding headers later?
If you also run Nginx in front of Apache or use a CDN, the final browser-visible behavior may differ from your Apache config alone. In mixed environments, compare origin responses with edge responses and document which layer owns which rule. For Nginx-side comparisons, see Nginx Caching Guide: FastCGI, Proxy Cache, and Static File Rules.
Common mistakes
This section helps you avoid the failure modes that cause most post-deploy cache confusion.
Applying the same header to every response
A single global rule is tempting, especially in .htaccess, but sites rarely have one universal caching need. HTML, hashed bundles, API responses, and user-specific pages usually need different treatment.
Using year-long caching on unversioned files
This is one of the most common causes of “it works for me but not for users” after a release. If a file URL stays the same, browsers may keep a stale local copy until the cache lifetime expires.
Using no-store when no-cache would do
no-store is appropriate for sensitive content, but it is often overused on ordinary HTML. That can prevent useful caching entirely and increase network traffic. If you mainly want freshness checks, revalidation is often enough.
Forgetting that HTML controls asset discovery
You may perfectly cache your JavaScript bundles, but if the browser keeps an old HTML page, it may never request the new bundle names. That is why HTML policy deserves separate attention.
Relying on Apache defaults without checking modules and overrides
Apache behavior varies by environment. Shared hosts, distro packages, control panels, and inherited configs may enable modules or add headers you did not expect. Always inspect the real response headers instead of assuming the config path is clean.
Ignoring validators in multi-layer caching
Validators can help, but they can also create complexity when origins, reverse proxies, and browsers behave differently. Be especially careful with ETag generation in clustered environments. Keep validator strategy simple unless you have a clear reason to do more.
Changing cache policy without connecting it to deployment
Cache headers are deployment policy expressed as HTTP. If your deploy process does not version assets, purge intermediaries when needed, and verify response headers after release, header changes alone will not solve the underlying problem.
When to revisit
The best Apache cache setup is not something you write once and forget. Revisit it whenever the way you build, publish, or serve files changes.
Review your configuration when:
- You move from unversioned assets to hashed build artifacts
- You introduce a CDN, reverse proxy, or another caching layer
- You change front-end frameworks or bundlers
- You split an app into multiple services or domains
- You start serving authenticated content from pages that were previously public
- You notice stale assets after deploys, too many 304 responses, or poor repeat-visit performance
- Your hosting environment changes Apache modules, default configs, or override rules
A practical review routine looks like this:
- List your response types — HTML, CSS, JS, images, fonts, API endpoints, downloads.
- Mark which URLs are versioned and which are stable.
- Assign a cache policy per class instead of per guess.
- Verify real headers in production with browser tools and header requests.
- Test after deployment using a clean browser session and a previously cached session.
- Document ownership so your team knows whether Apache, a proxy, or a CDN is the source of truth.
If you want a final cross-check before shipping changes, keep a short runbook: one HTML URL, one hashed asset URL, one unversioned file if any, and one authenticated page if relevant. Confirm expected headers on each. That small habit catches a surprising number of regressions.
For a broader production sanity check, return to HTTP Caching Checklist for Production Sites. And if you need to debug browser-side behavior during rollout, How to Debug Caching Issues in Chrome DevTools is a practical next step.
The durable rule is straightforward: cache static assets as aggressively as your URL versioning allows, keep HTML fresh enough to discover new releases, and test the actual headers your users receive. Apache directives may change from server to server, but that framework stays useful.