Cache-Control for SEO: What the Header Does and How to Set It
By Seth Black · 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