HTTP Cache Headers for Ecommerce: Browser Caching Strategy for Product Pages and Assets
Cache-Control headers determine whether browsers and CDNs store your pages and assets between visits. Set them right and repeat visitors load your store in under 1 second. Set them wrong and you're serving your JS bundle fresh on every click.
Browser caching is one of the highest-leverage performance optimizations available to ecommerce stores — and one of the most frequently misconfigured. A returning customer who already loaded your homepage CSS and JavaScript should get those files from their browser cache, not from your server or CDN. Whether they do depends entirely on the Cache-Control headers your server sends.
This guide covers the specific headers that matter, the right values for different asset types, and the caching patterns that apply specifically to ecommerce product pages.
Cache-Control Basics
The Cache-Control header tells browsers and intermediate caches (CDNs, reverse proxies) how long to store a response. The key directives:
- max-age=N — cache for N seconds.
max-age=31536000= one year. - s-maxage=N — CDN-specific max-age. Overrides max-age for shared caches only.
- no-cache — store the response, but revalidate with the server before using it. Not "don't cache."
- no-store — don't store anything. True "don't cache."
- immutable — tells the browser the resource will never change during its max-age. Prevents revalidation requests even on reload.
- stale-while-revalidate=N — serve the cached (potentially stale) response immediately while fetching a fresh one in the background.
- must-revalidate — must check with the server once the resource is stale (max-age expired).
The Right Strategy by Asset Type
Static assets with content hashing (JS bundles, CSS files)
Modern build tools (Webpack, Vite, Next.js) add content hashes to filenames: main.a3f9d8.js. When the file changes, the filename changes — so the URL is effectively a different resource. This means you can safely cache these forever:
Cache-Control: public, max-age=31536000, immutable
The immutable directive tells browsers not to revalidate this resource even on a hard reload, since the hash guarantees freshness. This is the optimal setting for any build-tool-generated asset with a content hash in the URL.
Images
Product images rarely change. When they do, the URL typically changes (new upload = new CDN path). Set a long cache with immutable:
Cache-Control: public, max-age=31536000, immutable
If your images use fixed URLs that can be replaced (like /images/product-main.jpg that gets overwritten on update), use a shorter max-age and drop immutable:
Cache-Control: public, max-age=604800
One week is usually right for product images with mutable URLs.
Web fonts
Fonts almost never change. Long cache:
Cache-Control: public, max-age=31536000, immutable
HTML pages (product pages, category pages)
This is where ecommerce caching gets nuanced. Your product pages contain dynamic information: price, stock status, promotional banners. You can't cache them forever — but you also don't want every Googlebot crawl or repeat visitor to wait for a full server-side render.
The standard approach for ecommerce HTML is short CDN caching with stale-while-revalidate:
Cache-Control: public, s-maxage=60, stale-while-revalidate=600
This tells CDNs to cache the page for 60 seconds, then serve the stale version for up to 10 more minutes while fetching a fresh copy in the background. Users almost always get a fast response; the stale window is bounded at 10 minutes. Adjust based on how frequently your prices or stock levels change.
For product pages where price changes are rare (B2B, slow-moving inventory), you can push this further:
Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400
One hour of definite freshness, 24 hours of stale-ok background refresh. Aggressive but valid for stable catalog stores.
Cart, checkout, and account pages
Never cache these at the CDN level. They contain user-specific state:
Cache-Control: private, no-store
Or at minimum:
Cache-Control: private, no-cache
The private directive prevents CDNs from caching the response. no-store prevents the browser from storing it at all.
API responses
Stock availability endpoints, price APIs, and cart APIs should not be cached:
Cache-Control: private, no-store, max-age=0
If you have public API responses that are safe to cache (e.g., a store's open hours that changes once a month), use short max-age with must-revalidate.
The Shopify CDN Advantage
Shopify stores automatically get assets served through Shopify's CDN with correct long-lived cache headers. Theme JS and CSS are content-hashed. Product images are served from cdn.shopify.com with one-year caching. You typically don't need to configure this — Shopify handles it.
What Shopify doesn't cache is the HTML pages themselves. Each product page request hits Shopify's servers (with their own edge caching layer). If TTFB is slow, it's usually the dynamic HTML render, not the assets.
Common Cache Header Mistakes
No-cache on static assets
The most expensive mistake: serving JS/CSS/images with Cache-Control: no-cache or no header at all. Every page load — including the second, third, and hundredth visit — downloads all assets fresh. StoreVitals flags pages where cacheable assets have no Cache-Control header or max-age=0.
Caching HTML pages too aggressively
A product page cached for 24 hours at the CDN will serve the old price for 24 hours after a price change. A sale banner that's active for 4 hours will appear on cached pages for hours after it ends. Match your HTML cache TTL to how frequently your page content actually changes.
Missing Vary headers
If you serve different content to mobile vs. desktop (different HTML, not CSS), you need Vary: User-Agent or risk CDNs serving mobile markup to desktop users. Most modern stores use responsive CSS and a single HTML document — this isn't an issue. But stores with separate mobile sites (/m/ subdomains or m.example.com) need correct Vary configuration.
ETags without max-age
ETags enable conditional requests (If-None-Match) but still require a round-trip to the server to validate. For static assets with content hashes, ETags add complexity without benefit — use max-age=31536000 + immutable instead and skip the validation round-trip entirely.
Checking Your Cache Headers
Browser DevTools → Network tab → click any resource → Headers → Response Headers. Look for Cache-Control in the response.
StoreVitals' Cache Headers Checker fetches any URL and shows the Cache-Control value for the main page and all linked assets, flagging missing or suboptimal values. Run it on your homepage, a product page, and your main JS bundle to see your current caching posture.