HTTP Cache Partitioning and Cross-Site Font Sharing
For years, a popular optimization rested on a single assumption: if thousands of sites loaded the same font file from the same CDN URL, a visitor who already had that file cached on one site would get it for free on the next. That assumption is now wrong. Modern browsers partition the HTTP cache by top-level site, so a font on a shared CDN is not reused across different sites — the old "shared Google Fonts cache" optimization is dead. This deep-dive on Browser Font Caching Mechanics explains the partitioning model and is part of the broader Font Loading & Delivery Strategies blueprint.
If you are still recommending a public CDN font URL "so it's already cached," you are shipping a slower first paint than self-hosting would, with none of the privacy or stability benefits. This page shows how to verify partitioning in DevTools and what to do instead.
Problem Statement
Chrome 86 (October 2020), Firefox 85 (January 2021), and Safari (which has partitioned for years via its own model) changed the HTTP cache key. A cached resource used to be keyed only by its URL. Now it is keyed by a tuple of the top-level site, the frame site, and the resource URL. Two different top-level sites loading the identical https://cdn.example/inter.woff2 get two independent cache entries. The byte-for-byte same font is downloaded again on the second site, even though the bytes already sit on disk under a different partition.
The change was made to close a privacy and security hole: a third party could probe the cache (via timing) to detect whether you had previously visited an unrelated site, enabling cross-site tracking and history sniffing. Double-keying eliminates that side channel. The cost is that cross-site resource reuse — the entire premise of "use a shared CDN font for the free cache hit" — no longer happens.
How the Cache Key Changed
The practical consequence: a font that is "popular across the web" provides zero head start on your site. The first time a visitor lands on your top-level site, every font is a cold fetch, regardless of how many other sites use the same CDN URL. The only cache reuse you still get is within your own site, across page navigations and repeat visits.
How to Verify Partitioning in DevTools
You can confirm the behavior directly rather than trusting release notes.
Diagnostic Steps
- Open Chrome DevTools → Network tab and load a page that pulls a font from a shared CDN.
- Note the font in the waterfall — its
Sizecolumn shows the transferred bytes (a real network fetch, not(disk cache)). - Navigate to a different top-level site that loads the same CDN font URL. Reload. The same font fetches again with full transferred size — proof it was not reused across the site boundary.
- Now reload a page on the same site. This time the
Sizecolumn reads(disk cache)or(memory cache)— within-site reuse still works. - In Chrome, open
chrome://net-export/, capture a session, and inspect thenetwork_isolation_keyfield on the request — it includes the top-level site, demonstrating the partition. - In Application → Storage, note that cache entries are scoped per partition; there is no global, URL-only font store any more.
What To Do Instead: Self-Host with Immutable Caching
Since cross-site reuse is gone, the winning strategy is self-hosting your fonts on your own origin and maximizing within-site repeat-visit caching. This also removes a third-party connection from your critical path and avoids the extra DNS + TLS round trips of a separate font CDN. For the full trade-off analysis, see Google Fonts vs Self-Hosting.
The core technique is content-hashed filenames plus a long, immutable Cache-Control header. Because the hash changes whenever the font bytes change, you can cache aggressively forever and never worry about stale assets.
Immutable cache headers for content-hashed fonts (nginx)
# Match content-hashed font files, e.g. inter-latin.a1b2c3d4.woff2
location ~* "-[0-9a-f]{8,}\.woff2$" {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "*"; # only if served cross-origin
types { font/woff2 woff2; }
expires 1y;
}
Line-by-line:
location ~* "-[0-9a-f]{8,}\.woff2$"— only files whose name carries an 8-plus-character hex content hash get the immutable policy. Un-hashed fonts must not be cached this way, because their URL never changes when the bytes do.max-age=31536000— one year in seconds, the maximum the spec recommends. The browser may serve from disk for a year without revalidating.immutable— tells the browser the response body will never change at this URL, so it skips conditional revalidation (noIf-None-Matchround trip) even on a hard reload in supporting browsers. This is the part that makes repeat visits truly zero-latency for fonts.public— the response may be stored by shared caches (your CDN, proxies), not just the private browser cache.Access-Control-Allow-Origin— required only if the font is fetched from a different origin than the page; preloaded fonts always need CORS, so keep it if you front the assets with a CDN on a separate hostname.expires 1y— emits a matchingExpiresheader for legacy intermediaries.
Pair this with content hashing in your build so the filename rotates automatically:
Content-hashed output via a bundler (Vite example)
// vite.config.js
export default {
build: {
assetsInlineLimit: 0, // never inline fonts; keep them cacheable files
rollupOptions: {
output: {
assetFileNames: 'assets/[name].[hash][extname]' // inter-latin.a1b2c3d4.woff2
}
}
}
}
The [hash] token is derived from file contents. Ship a new font, the hash changes, the URL changes, and the immutable cache is correctly busted — no manual versioning, no ?v=2 query strings.
Defensive Variant: stale-while-revalidate for Non-Hashed Fonts
If you cannot content-hash a font URL — for example a legacy path you must keep stable, or fonts injected by a third-party widget — immutable is unsafe because you would serve stale bytes after an update. Use stale-while-revalidate instead. It serves the cached copy instantly while fetching a fresh one in the background, so users never block on revalidation but eventually pick up changes.
Safe caching for a stable (non-hashed) font URL
location = /fonts/brand-icons.woff2 {
# Serve fresh for 1 day; for the next 7 days serve stale instantly
# while revalidating in the background.
add_header Cache-Control "public, max-age=86400, stale-while-revalidate=604800";
types { font/woff2 woff2; }
}
Here max-age=86400 keeps the asset fresh for a day; stale-while-revalidate=604800 lets the browser keep using the (now stale) cached file for up to seven more days while it quietly fetches an update, so an icon-font change propagates within a week with no blocking fetch on the hot path. This is strictly worse than the hashed-immutable approach for fonts you control, so reserve it for URLs you cannot rotate.
Verification
Confirm both the caching policy and the within-site reuse:
- Header check —
curl -sI https://your-site/assets/inter-latin.a1b2c3d4.woff2 | grep -i cache-controlshould printpublic, max-age=31536000, immutable. - Repeat-visit check — in DevTools Network, do a first load (font shows transferred bytes), then a second navigation on the same site; the font should read
(disk cache)with200 (from disk cache)and no outgoing conditional request. - No revalidation — with
immutable, even a normal reload should not emitIf-Modified-Since/If-None-Matchfor the font. If you see a304, theimmutabledirective is missing or the browser predates support. - Lighthouse — the "Serve static assets with an efficient cache policy" audit should stop flagging your fonts once the year-long max-age is in place.
Common Pitfalls
- Recommending a public font CDN URL "for the shared cache" — that reuse no longer exists post-partitioning, so you only pay the extra connection cost with no caching upside.
- Applying
immutableto a font URL that is not content-hashed — visitors keep stale glyphs for up to a year after you update the file. - Forgetting the
crossoriginattribute when self-hosted fonts are preloaded from a CDN subdomain — the preload and the@font-facefetch use different cache buckets and the font downloads twice. See browser font caching mechanics for the matching-request rules. - Expecting Safari to behave like the old single-key model — Safari has partitioned for years and additionally caps cache lifetimes more aggressively, so cross-site assumptions break there even harder.
- Versioning fonts with
?v=2query strings instead of hashing the filename — many proxies and CDNs ignore query strings for cache keys, so the bust may not propagate.
FAQ
Can I still benefit from a shared CDN for fonts after partitioning? Not for cross-site cache reuse — that is gone in Chrome 86+, Firefox 85+, and Safari. A CDN still helps by serving bytes from an edge node close to the user, but you get the same edge benefit (plus one fewer cross-origin connection) by putting hashed font files on your own origin behind a CDN. Self-hosting with immutable caching is the recommended path.
Does partitioning hurt repeat visits to my own site?
No. Partitioning only blocks reuse across different top-level sites. Within your own site — across pages and return visits — the cache works exactly as before, which is why a one-year immutable policy on content-hashed fonts gives near-instant repeat renders.
How do I prove a font was served from cache and not revalidated?
In DevTools Network, the Size column shows (disk cache) and the Status shows 200, not 304. With Cache-Control: ...immutable, a reload should issue no conditional request at all; seeing a 304 means the immutable directive is missing or unsupported in that browser.