Skip to main content
Docs menu

Set Cache-Control headers

Last updated Edit this page View as Markdown

Which Cache-Control headers to set on your origin, by content type and by framework, for correct caching with mod_pagespeed 2.1.

On this page

Your origin’s Cache-Control headers directly control how mod_pagespeed caches and serves your content. Getting these right prevents stale content and cuts unnecessary origin traffic.

When your origin sends no Cache-Control header, mod_pagespeed applies configurable defaults (HTML: no-cache, CSS/JS: 300s, images: 1800s in safe mode) and logs a warning. Explicit headers are better than relying on these fallback TTLs.

HTML pages

Cache-Control: public, max-age=60, must-revalidate
ETag: "content-hash-or-version"

HTML references hashed assets. A deploy changes HTML content to point at new asset URLs. Short max-age=60 provides one minute of caching. With conditional revalidation, stale requests are cheap — origin returns 304 when content hasn’t changed.

Use max-age=0 only when absolute real-time freshness is required (stock tickers, live scores). Every request with max-age=0 triggers a conditional revalidation, which means an origin round-trip even when content hasn’t changed.

Hashed static assets (CSS/JS with fingerprints)

Cache-Control: public, max-age=31536000, immutable

The URL changes on every build. Content at a given URL never changes. mod_pagespeed strips immutable from its transformed output — the optimized bytes depend on mutable config and software version — but preserves the long TTL, capping max-age at pagespeed_immutable_max_age (default: 7 days).

Non-hashed static assets

Cache-Control: public, max-age=3600, must-revalidate
ETag: "file-mtime-and-size"

The URL is stable but content may change. Short max-age with must-revalidate keeps content fresh. Conditional revalidation makes the revalidation cheap.

User-uploaded images

Cache-Control: public, max-age=86400

User images rarely change at the same URL. 24-hour caching is reasonable. If images can be replaced at the same URL, add must-revalidate and an ETag.

API responses and dynamic content

Cache-Control: no-store

mod_pagespeed does not cache no-store responses. The response passes through unchanged.

Framework configuration

FrameworkWhere to Set HeadersHTMLHashed Assets
nginx (static)nginx.conf per locationmax-age=60, must-revalidatemax-age=31536000, immutable
Apache.htaccess Header setmax-age=60, must-revalidatemax-age=31536000, immutable
Next.js 13+next.config.js headers()max-age=60, must-revalidateAutomatic for _next/static/
Astro 4+Server adapter headersmax-age=60, must-revalidateAutomatic via content hashing
Rails 7+config.public_file_server.headersmax-age=60, must-revalidateAutomatic via Propshaft
WordPressCache plugin or .htaccessmax-age=60, must-revalidateTheme-dependent

PURGE vs natural expiration

PURGE immediately invalidates all cached variants for a URL. Use it for urgent content corrections. For routine deploys, let must-revalidate handle freshness — conditional revalidation is cheaper than purge-and-rebuild because it preserves optimized image variants (AVIF, WebP) when only the HTML changed. See 304 conditional revalidation vs PURGE for when each is the right invalidation strategy.

Cache mode

The pagespeed_cache_mode directive controls how mod_pagespeed assembles Cache-Control headers on optimized responses. Safe mode (the default) adds must-revalidate and uses short TTLs for quick recovery from misconfigurations. Aggressive mode uses long TTLs with public for maximum cache efficiency. See Cache Modes for full details, or the safety math behind must-revalidate vs aggressive TTLs for how each mode trades freshness against origin load.

mod_pagespeed directives

These directives control the default max-age values applied when origin responses lack a Cache-Control header. They do not override explicit origin headers.

pagespeed_html_max_age 0;      # Default for HTML (seconds, default: 0 = no-cache)
pagespeed_css_max_age 300;     # Default for CSS/JS (seconds, default: 300)
pagespeed_image_max_age 1800;  # Default for images (seconds, safe: 1800, aggressive: 86400)

When pagespeed_html_max_age is 0 and the origin sends no Cache-Control, HTML responses are served with Cache-Control: no-cache — the client must revalidate on every request.

Conditional revalidation

pagespeed_conditional_revalidation on;   # default: on

When enabled, stale cache entries with stored ETag or Last-Modified values trigger conditional requests (If-None-Match / If-Modified-Since) instead of full re-fetches. On 304, the cached content is refreshed without re-downloading or re-optimizing. Disable only if your origin mishandles conditional requests.

Browser force-refresh

pagespeed_force_refresh_html on;   # default: on
pagespeed_force_refresh off;       # default: off

When a user performs a force-refresh (Ctrl+F5 or Shift+Reload), the browser sends Cache-Control: no-cache or Pragma: no-cache. mod_pagespeed detects these signals and forces revalidation against your origin, even if the cached entry is fresh.

For HTML, this is enabled by default — users expect force-refresh to fetch the latest page. For images, CSS, and JS, it is off by default to prevent unnecessary origin load. Enable pagespeed_force_refresh if you want force-refresh to apply to all content types.

When conditional revalidation is also enabled (the default), force-refresh uses If-None-Match / If-Modified-Since to avoid re-downloading unchanged content.

Cache-extension filters

The filters in this section belong to the in-process module on Apache, nginx and IIS. They extend browser cache lifetimes and manage resource URLs across domains. Browsers cache resources for a year, but pick up updated content the moment a resource changes, because the URL carries a content hash. The extend_cache CoreFilter drives this: it sets a 1-year cache lifetime on CSS, JS, and image URLs.

extend_cache

Full guide →

Core filter. extend_cache rewrites the URLs of stylesheets, scripts, and images to carry a content hash, giving each resource a .pagespeed.ce. URL, and serves those resources with a one-year Cache-Control: max-age. When the original file changes, its hash changes too: once the module’s cached copy of the original expires (origin lifetime, or ImplicitCacheTtlMs when none is set), the page points at a new URL. Live demo: extend_cache.

<!-- before -->
<link rel="stylesheet" href="/css/site.css" />

<!-- after: served with Cache-Control: max-age=31536000 -->
<link rel="stylesheet" href="/css/site.css.pagespeed.ce.HASH.css" />

When it helps and when it does not

It helps wherever the origin cannot set long cache lifetimes itself, which is common on shared hosting and legacy applications: first visits cache every static resource for a year, and repeat visits stop revalidating them. It does nothing for resources that already carry a fingerprint and a long max-age from the build pipeline, since those URLs are already immutable in practice. It also does not make HTML cacheable: the page itself keeps its own short lifetime, because the page is what hands out the new hashed URLs after a deploy.

How it decides

Only resources on domains the module is authorized to rewrite are candidates, and resources already rewritten by another filter (a minified stylesheet, an optimized image) already carry a content hash, so there is nothing to extend. The one-year lifetime works because the URL is derived from the bytes: changed content gets a new URL rather than new bytes at the old one, so visitors pick it up once they load a page that references the new URL. The name is compound: it switches on extend_cache_css, extend_cache_images, and extend_cache_scripts, and each member can be disabled on its own.

Risks

  • HTML and the resources it references must share one configuration; a setup where two virtual hosts disagree on the same file can serve a hash that does not match. See Virtual hosts.
  • Verify with the X-Mod-Pagespeed response header and a ?PageSpeedFilters=-extend_cache comparison; Is it working? has the steps.

Configuration

# Apache
ModPagespeedEnableFilters extend_cache
# Nginx
pagespeed EnableFilters extend_cache;

The sub-filters extend_cache_css, extend_cache_images, and extend_cache_scripts are included when you enable extend_cache, which turns on all three. Each can also be enabled individually with EnableFilters (for example, extend_cache_images alone).

extend_cache_pdfs

Full guide →

Not a CoreFilter. Applies the same content-hash-based cache extension to PDF file links. Enable this filter if your site serves PDFs that change infrequently.

# Apache
ModPagespeedEnableFilters extend_cache_pdfs
# Nginx
pagespeed EnableFilters extend_cache_pdfs;

rewrite_domains

Full guide →

Not a CoreFilter. Test before deploying. Rewrites resource URLs to use domains specified by MapRewriteDomain or ShardDomain directives. Useful for CDN integration or domain sharding.

# Apache
ModPagespeedEnableFilters rewrite_domains
# Nginx
pagespeed EnableFilters rewrite_domains;

This filter only affects resources that mod_pagespeed does not otherwise optimize. Resources already rewritten by other filters (rewrite_css, rewrite_images, etc.) already have their domains set by those filters.

See Domain configuration for MapRewriteDomain and ShardDomain setup.

local_storage_cache

Full guide →

Experimental. local_storage_cache moves the bytes of inlined images and CSS out of the HTML and into the browser’s localStorage. On the first visit the inlined resource is served as usual and a script stores its bytes under a pagespeed_lsc_url: key; on later visits the module sees its own _GPSLSC cookie saying the copy is there, and serves a one-line script in place of the data, which restores the resource from localStorage. The HTML shrinks on every repeat view by the size of everything stored. Not a core filter; enable it by name, together with the filters that inline (inline_images, inline_css). Live demo: local_storage_cache.

<!-- first view: the stylesheet is inlined, marked for storage -->
<style data-pagespeed-lsc-url="https://example.com/css/site.css" ...>
  .site { ... }
</style>

<!-- repeat view: one script line instead of the whole stylesheet -->
<script>
  pagespeed.localStorageCache.inlineCss('https://example.com/css/site.css');
</script>

The mechanism is per-resource and expiry-aware: each stored entry carries the resource’s cache expiration, expired entries are dropped from storage and from the cookie, and an element whose entry is missing or expired is restored by fetching the resource from the network. The filter needs JavaScript to run: since v1.15.0+r18, with HonorCsp (default on) it stands down on pages whose Content-Security-Policy disallows the inline script it relies on.

Not recommended for most deployments. Browser localStorage has size limits (typically 5-10 MB per origin) and can be cleared by the user at any time. Sites with many inlined resources may exceed these limits, and pages then pay the storing overhead without the repeat-view saving. A page whose HTML is already cached by the browser gains little: the inlined bytes ride inside a cached document either way. Compare it against extend_cache, which gets the same repeat-view saving by keeping resources in external, content-hashed URLs with a one-year lifetime and no scripts at all; that is the recommended path.

# Apache
ModPagespeedEnableFilters local_storage_cache
# Nginx
pagespeed EnableFilters local_storage_cache;

On IIS the same directives use the syntax described in IIS configuration.

Auditing your origin

The web console flags URLs where the origin sends no Cache-Control header. Use the URL inspector to check freshness status and identify origins that need explicit headers.

Search