Cache-Control headers

Delivery

Cache-Control headers are HTTP directives that tell browsers, CDNs, and other intermediaries how to cache, reuse, or revalidate responses. For images, they determine whether a file can be stored, for how long, and under what conditions it must be checked with the origin again. Effective Cache-Control policies reduce repeat downloads, cut bandwidth, and improve repeat-view performance metrics, while poor settings can cause stale images or unnecessary validation traffic. The header works alongside validators like ETag and Last-Modified, and can be tuned separately for browsers and shared caches via directives such as s-maxage.

Definition and purpose

Cache-Control provides explicit, machine-readable rules for how responses should be stored and reused across the web delivery chain. In the context of images—which often represent the majority of transferred bytes on content-heavy sites—sound caching directives minimise redundant transfers on subsequent pageviews, across page navigations, and for returning visitors. They also guide shared caches like CDNs to serve many users from a nearby edge node, reducing origin load and egress costs. The purpose is to balance freshness and correctness with performance, factoring in asset mutability, user-specific content, and operational constraints.

Below are the primary Cache-Control directives and what they mean in HTTP caching, with emphasis on responses serving images. Where relevant, scope is noted: “response” (server to caches/clients) or “request” (client to caches/origin).

Response directives (server → caches/clients)

  • max-age=N: Time in seconds a response is considered fresh in browsers and private caches. Common for static, fingerprinted images (e.g., max-age=31536000).
  • s-maxage=N: Freshness lifetime for shared caches (CDNs, proxies). Overrides max-age in shared caches; useful when browser and CDN TTLs should differ.
  • public: Explicitly allows storage by any cache, including shared caches. Typical for publicly accessible image assets.
  • private: Restricts caching to the end-user’s browser; shared caches should not store. Use when image content might be user-specific (e.g., profile images in authenticated contexts).
  • no-store: Do not write to any cache. Appropriate for sensitive or highly dynamic images; otherwise harmful to performance if applied broadly to static assets.
  • no-cache: Allows storage but requires successful revalidation before reuse. Use when correctness is critical but storage is acceptable to reduce payload on 304 responses.
  • must-revalidate, proxy-revalidate: Once stale, caches must revalidate with the origin; proxy-revalidate targets shared caches specifically. Useful when serving time-sensitive imagery that must not be used stale.
  • immutable: Indicates content will not change during its freshness lifetime; browsers can skip conditional checks on reload. Best with long-lived, versioned images.
  • stale-while-revalidate=N: Allows serving stale content while a background revalidation fetch occurs. Improves tail latency when the item is near or past expiry in shared caches.
  • stale-if-error=N: Permits serving stale content if the origin returns an error, increasing resilience during outages for non-critical images like thumbnails.

Request directives (client → caches/origin)

  • max-age=N (request): Client is willing to accept a response up to N seconds old. Used less commonly for images compared to response directives.
  • min-fresh=N: Client requires the response to remain fresh for at least N more seconds. Can prompt cache misses if the stored copy is close to expiry.
  • max-stale[=N]: Client will accept stale responses, optionally up to N seconds. Sometimes observed with offline-tolerant strategies or service workers.
  • only-if-cached: Client wants a cached response and will not trigger a network fetch. Useful in constrained environments; may return a 504 if nothing is cached.

Cache types and scope

Cache-Control directives apply across private caches (browsers) and shared caches (reverse proxies, CDNs). Browser caches include in-memory and disk storage, and may be complemented by Service Worker Cache Storage, which your app controls. Shared caches sit between clients and origin servers, serving many users and often enforcing their own limits, defaults, and normalisation rules. The public/private distinction informs whether shared caches may store the response, while max-age and s-maxage communicate freshness lifetimes appropriate to each layer.

Images that are identical for all users are excellent candidates for shared caching with long TTLs, especially when file names are versioned via content hashing. Conversely, user-specific or sensitive images should be scoped to private caches or disabled entirely using private and/or no-store. When content negotiation is in play—for example, serving WebP or AVIF based on Accept, or DPR/Width via Client Hints—ensure that Vary headers are correctly set so that caches store separate variants without confusion.

Performance impact

For first-time visitors, Cache-Control has limited impact beyond enabling CDN edge hits; for subsequent navigations or repeat visits, it dramatically reduces bytes transferred and request latency. Long-lived, immutable caching of static images removes network dependency for those assets, improving repeat-visit page load and reducing battery usage on mobile. Shared cache directives such as s-maxage and stale-while-revalidate help maintain high edge hit ratios, smoothing origin traffic and improving tail latencies during refresh cycles. While Core Web Vitals are measured per pageview, field data often benefits on multi-page sessions where re-used imagery feeds faster rendering on later pages.

Operationally, fewer origin requests translate into lower egress and compute costs and increased resilience under load. Conditional requests (ETag/If-None-Match, Last-Modified/If-Modified-Since) still incur network round-trips even when returning 304 Not Modified, so favour true cache hits with long TTLs for static, versioned assets. For dynamic or frequently changing images (for example, promo banners), use shorter TTLs on browsers and longer TTLs on shared caches combined with revalidation and stale-while-revalidate to balance freshness with performance. Expect real-world improvements to vary based on the proportion of repeat traffic and common asset reuse across pages.

Common misconfigurations and risks

Typical pitfalls include marking static images as no-store, which eliminates cache benefits, or using private unnecessarily, which prevents CDN caching. Long TTLs without asset versioning risk stale content after updates; pair max-age=31536000 with fingerprinted file names and immutable to avoid serving outdated images. Another frequent issue is omitting s-maxage when a CDN is present, leading to lower edge hit rates than intended because shared caches fall back to the shorter browser max-age. For negotiated formats and responsive imagery, missing or incorrect Vary headers can cause caches to serve the wrong variant to some clients.

Care is also required with no-cache versus no-store: the former allows storage but forces validation, while the latter forbids storage entirely. Combining immutable with short max-age can be contradictory, and mixing must-revalidate with stale-while-revalidate may cause confusing behaviour across intermediaries. Validators that change on every deploy (e.g., weak ETags that include timestamps) defeat efficient revalidation. Lastly, avoid caching HTML with overly permissive directives that can cause page content to be served stale; reserve aggressive caching for static assets like images, fonts, and versioned scripts.

Scope

Scope operates on two axes: cache audience (private vs shared) and directive direction (request vs response). Public/private governs which caches may store the response, with public suitable for non-sensitive images and private for user-specific content. Response directives like max-age, s-maxage, no-store, and immutable tell caches what to do with the response; request directives like max-stale and only-if-cached express client preferences to caches and the origin. In multi-layer delivery stacks, shared caches may obey s-maxage while browsers follow max-age, enabling different policies per layer from the same response.

Implementation notes

Suggested patterns for images

- Static, fingerprinted assets: Cache-Control: public, max-age=31536000, immutable. Pair with content-hashed file names to allow long TTLs without risking staleness.
- CDN-backed dynamic thumbnails: Cache-Control: public, s-maxage=86400, max-age=600, stale-while-revalidate=600, stale-if-error=86400. Revalidate with ETag or Last-Modified.
- User-specific images: Cache-Control: private, max-age=0, no-cache (or no-store for sensitive). Avoid shared caching and ensure correct authentication controls.
- Negotiated formats or client hints: Add Vary: Accept, Accept-Encoding, DPR, Width, Save-Data so caches keep distinct variants per capability.

Operational considerations

Prefer response headers over per-request query conventions for controlling cache unless your CDN supports rules at the edge. Align CDN behaviour with origin headers; if your CDN recognises Surrogate-Control, you can separate edge TTLs from downstream cache behaviour. Monitor cache hit ratios, revalidation rates, and 304 vs 200 distributions to validate policy effectiveness, and keep an eye on header size limits and normalisation behaviour that might strip or rewrite directives. When rotating assets, use deploy-time invalidation only as a backstop; primary control should be via URL versioning of images to retain long-lived caching safely.

Comparisons

Cache-Control vs Expires

Expires sets an absolute expiry date; Cache-Control uses relative times and more nuanced directives. When both are present, Cache-Control generally takes precedence in modern caches. Expires can act as a fallback for legacy intermediaries, but using only Cache-Control is sufficient for most contemporary delivery chains. Avoid relying solely on Expires due to clock skew risks and less expressive control.

s-maxage vs Surrogate-Control

Both target shared caches. s-maxage is part of Cache-Control and widely supported; Surrogate-Control is a non-standard header some CDNs interpret to define edge-specific TTLs and behaviours. If your CDN honours Surrogate-Control, you can keep conservative browser TTLs while giving the edge a longer lifetime. Where not supported, s-maxage is the portable way to set shared cache freshness distinctly from browser max-age.

no-cache vs no-store vs immutable

no-cache allows storage but requires revalidation before reuse, which can still save bytes via 304 responses. no-store forbids any storage and should be reserved for highly sensitive or rapidly changing resources. immutable tells browsers that the content will not change while fresh, avoiding conditional requests on reload. For versioned images, immutable with a long max-age is preferred; for frequently updated images, shorter max-age with revalidation is more appropriate than no-store in most cases.

FAQs

Should images use immutable by default?

Yes for versioned, static images whose URLs change on content updates (e.g., hashed file names). immutable reduces unnecessary conditional requests on reload or navigation. Avoid immutable on unversioned or frequently updated assets because it can prolong staleness until expiry. Pair immutable with long max-age and a reliable asset versioning strategy.

How do max-age and s-maxage interact with a CDN?

Shared caches honour s-maxage when present; otherwise they fall back to max-age. This lets you set, for example, max-age=600 for browsers while giving the CDN s-maxage=86400 to keep assets longer at the edge. Ensure your CDN does not override origin cache headers unless intentionally configured; many provide defaults that can shorten or lengthen TTLs.

Do long cache lifetimes affect SEO negatively?

Not when combined with URL versioning. Search engines fetch images by URL; if the URL changes when the image changes, crawlers and users receive the updated content immediately while old URLs remain cached safely. Long-lived caching improves crawl efficiency and user performance without harming indexation, provided you avoid serving changed content at the same URL for extended periods.

Is a query string enough for cache busting of images?

Most caches treat different query strings as distinct URLs, so adding a version parameter can bust caches. However, some CDNs normalise or ignore query parameters by policy, and intermediaries may restrict caching when a URL has a query string. Content-hashed file paths are generally more robust and CDN-friendly for dependable cache busting at scale.

What is the difference between 304 revalidation and a cache hit for images?

A cache hit serves the image from the cache with no network fetch to the origin, minimising latency and bandwidth. A 304 Not Modified response occurs after a conditional request checks the validator; it still incurs a round-trip and some overhead but avoids retransmitting the image bytes. Aim for true hits for static assets, and reserve revalidation for content that might change at the same URL.

Synonyms

Cache-ControlHTTP cache headersCaching headersHTTP Cache-ControlCache directives