Apache Cache Headers Guide for Static Assets and HTML
apachecache-headersmod-expiresperformanceserver-configstatic-assets

Apache Cache Headers Guide for Static Assets and HTML

ccached.space Editorial
2026-06-10
9 min read

A practical Apache cache headers guide for static assets and HTML, with clear rules, examples, and troubleshooting advice.

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-Control response headers
  • Expires headers, often managed through mod_expires
  • Validators like ETag and Last-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-store for highly dynamic or sensitive pages
  • Revalidation-first: Cache-Control: no-cache with validators, so the browser checks before reuse
  • Short freshness window: a small max-age for 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_expires for setting Expires and cache age by MIME type
  • mod_headers for directly setting or modifying Cache-Control and 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-Modified is simple and often sufficient for static files
  • ETag can 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.js

Check 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:

  1. List your response types — HTML, CSS, JS, images, fonts, API endpoints, downloads.
  2. Mark which URLs are versioned and which are stable.
  3. Assign a cache policy per class instead of per guess.
  4. Verify real headers in production with browser tools and header requests.
  5. Test after deployment using a clean browser session and a previously cached session.
  6. 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.

Related Topics

#apache#cache-headers#mod-expires#performance#server-config#static-assets
c

cached.space Editorial

Senior SEO Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

2026-06-10T00:26:36.814Z