Cache-Control for SEO: What the Header Does and How to Set It

By · Published

Cache-Control is the HTTP response header that tells browsers, CDNs and other caches whether they may store a response and for how long. It isn’t a ranking factor on its own. It matters for SEO in two indirect ways: cached pages and assets load faster for real users, and a server that answers revalidation requests cheaply lets crawlers spend their requests on pages that changed.

What does Cache-Control do?

A response carries one header with one or more directives:

HTTP/1.1 200 OK
Content-Type: text/css
Cache-Control: public, max-age=31536000, immutable

These are the directives you’ll actually use. Definitions follow MDN’s Cache-Control reference:

Directive What it means
max-age=N The response is fresh for N seconds.
s-maxage=N Same, but only for shared caches such as a CDN. Overrides max-age there.
no-cache May be stored, but must be revalidated with the origin before each reuse.
no-store Must not be stored by any cache.
private Only the browser may store it, not a CDN.
public Shared caches may store it.
must-revalidate Once stale, must be revalidated before reuse.
immutable Won’t change while fresh, so don’t revalidate even on reload.
stale-while-revalidate=N After it goes stale, a cache may serve it for N more seconds while refreshing in the background.

The one people get backwards: no-cache doesn’t mean “don’t cache”. It means “check before you reuse”. If you mean “never store this”, that’s no-store.

Does Cache-Control affect SEO?

Not directly. There’s no ranking signal for the header. What it changes:

Speed for real users. Core Web Vitals come from real visits, and repeat visits with cached CSS, JavaScript, fonts and images render faster. HTML cached at a CDN edge cuts time to first byte for everyone, which feeds Largest Contentful Paint. For more on reading those numbers per page, see how to diagnose Core Web Vitals failures at the page level.

Crawl efficiency. Google’s crawlers support conditional requests. Google’s crawler documentation says its crawling infrastructure supports ETag with If-None-Match and Last-Modified with If-Modified-Since, prefers ETag when both are present, and that other HTTP caching directives aren’t supported. Setting max-age alongside Last-Modified is described as optional and as something that can help crawlers decide when to recrawl. In practice: send a stable ETag, answer 304 Not Modified when nothing changed, and Googlebot gets a cheap response instead of a full page.

robots.txt. Google’s robots.txt specification says Google generally caches robots.txt for up to 24 hours and may adjust that based on max-age. A robots.txt change can take a day to be seen, and a long max-age on it is a way to make that worse.

What should Cache-Control be for each kind of file?

Resource Suggested header Why
Fingerprinted assets (app.3f9a1c.js) public, max-age=31536000, immutable The URL changes when the file changes, so cache it for a year.
Assets without a fingerprint public, max-age=86400 or less Nothing tells caches when the file changed.
Public HTML pages public, max-age=0, s-maxage=300, stale-while-revalidate=60 Browsers always revalidate; the CDN holds it briefly.
Logged-in or personal pages private, no-store Never let a CDN serve one user’s page to another.
robots.txt and sitemaps public, max-age=3600 Short enough that changes land quickly.

The rule underneath all of it: long cache lifetimes only on URLs that change when the content changes. That’s what fingerprinting (a content hash in the file name) is for.

How to set Cache-Control in nginx

# Fingerprinted static assets: one year, never revalidate
location ~* \.(?:css|js|woff2|png|jpg|jpeg|webp|avif|svg)$ {
    add_header Cache-Control "public, max-age=31536000, immutable" always;
}

# HTML: browsers revalidate, CDN keeps it five minutes
location / {
    add_header Cache-Control "public, max-age=0, s-maxage=300, stale-while-revalidate=60" always;
}

nginx sends ETag for static files by default. The same add_header inheritance trap I described in the X-Robots-Tag post applies here: a location with its own add_header drops every add_header from the server block.

How to set Cache-Control in Apache

<IfModule mod_headers.c>
  <FilesMatch "\.(css|js|woff2|png|jpe?g|webp|avif|svg)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
  </FilesMatch>
  <FilesMatch "\.html$">
    Header set Cache-Control "public, max-age=0, s-maxage=300"
  </FilesMatch>
</IfModule>

Only use the one-year rule if your build fingerprints file names. If it doesn’t, a year-long cache means visitors keep last month’s CSS.

How do you check a page’s Cache-Control?

curl -s -D - -o /dev/null https://example.com/ | grep -iE 'cache-control|etag|last-modified|age:'

Then test revalidation by sending the ETag back:

curl -s -o /dev/null -w '%{http_code}\n' \
  -H 'If-None-Match: "abc123"' https://example.com/

A 304 means conditional requests work. A 200 with the same ETag means the server ignores them and sends the full page every time. Behind a CDN, also check the CDN’s own status header (cf-cache-status, x-cache and similar) to see whether the edge served it.

Common Cache-Control mistakes

Caching HTML for a long time. A page with max-age=86400 keeps serving yesterday’s version, including yesterday’s noindex or broken canonical, until it expires. Keep HTML short-lived and purge the CDN on deploy.

Caching errors and redirects. A CDN that caches a temporary 404 or a 302 for hours turns a blip into an outage for crawlers. Give error responses a short lifetime or none.

no-store everywhere. Some frameworks send it on every response by default. That throws away all caching for public pages and forces full downloads for users and crawlers alike.

Year-long caching without fingerprints. The asset never updates for returning visitors.

What bseoa checks, and what it doesn’t

Honest limitation first: bseoa’s page analysis doesn’t fetch every subresource’s headers, so it isn’t the tool for auditing Cache-Control on each CSS or image file. Use curl or the browser’s Network panel for that, and read our reference pages on a missing Cache-Control header and insufficient cache time for the fixes. What a crawl does give you is time to first byte for every page: bseoa’s website crawler records ttfb_millis per URL in its jsonl-summary output, so sorting by it shows which pages your CDN isn’t serving from cache.

-Sethers

Discover hundreds of SEO Issues in Seconds

Without Monthly Subscriptions

Comprehensive technical SEO analysis powered by ML and 16 specialized modules. Optional AI-powered insights from Claude, GPT-4, or Gemini. Get actionable insights in seconds, and never pay monthly fees again.

Download Free Trial

I use AI to generate images for my posts and for general editing, updates, and ironically SEO purposes. I used to draw all of the images for my personal blog (taleas) myself, but as the volume of content I produce has increased, I've turned to AI tools to help create visuals that complement my writing. I go out of my way to generate images that look strange, and don't represent real people. If you ever want to chat about my use of AI, please reach out.