How to use font fingerprints for cache-busting and sri
- Step 1Fingerprint the production font — Drop the exact file you will deploy. The output's `cache_busted_filename` is `<stem>.<short8>.<ext>` — the extension is taken from your original filename, so a `.woff2` stays `.woff2`. The `short_hash` is the first 8 hex chars of the full `sha256`.
- Step 2Rename the deployed asset (filename hashing — recommended) — Deploy the font under `cache_busted_filename`. A new byte changes the hash, changes the filename, and a new filename is a brand-new cache entry — guaranteed invalidation. This is what `[contenthash]` in Webpack/Vite/Rollup automates; the Fingerprinter just shows you the value for a one-off.
- Step 3Set a long immutable cache header — Because the filename changes when the content changes, you can safely send `Cache-Control: public, max-age=31536000, immutable` on the hashed file. Returning visitors get an instant cache hit with zero bytes transferred until the next version ships under a new name.
- Step 4Reference the hashed name from CSS — Point `@font-face { src: url(...) }` at the hashed filename. Update the URL whenever the hash changes — your build tooling does this automatically if it owns both the rename and the CSS emit.
- Step 5Add SRI on third-party-served fonts — Paste the `sri_attribute` onto `<link rel="preload" as="font" crossorigin>`. The browser verifies the downloaded bytes against the hash. This is orthogonal to cache-busting — use it alongside filename hashing, not instead of it.
- Step 6Avoid query-string versioning for fonts — Do not rely on `font.woff2?v=<hash>`. Some CDNs strip the query string from the cache key, so `?v=1` and `?v=2` resolve to the same cached object and your update never reaches users. If you must use a query string (no rename control), confirm your CDN includes it in the cache key first.
The three strategies compared
What each pattern does, what the Fingerprinter gives you for it, and the catch. Filename hashing is the default recommendation for static font assets.
| Strategy | What it does | Fingerprinter field | Catch |
|---|---|---|---|
| Content-hashed filename | New bytes → new filename → new cache entry. Pair with immutable | cache_busted_filename | Requires control over the deployed filename + the CSS that references it |
| Query-string version | Same filename, ?v=<hash> appended | Use short_hash as the value | Some CDNs drop the query from the cache key → stale font served silently |
| Subresource Integrity | Browser verifies downloaded bytes against the hash; not a cache strategy | sri_attribute | Needs crossorigin; any byte change without a hash update fails the load |
Cache headers that pair with each strategy
The header you can safely send depends on whether the URL changes when the content changes.
| Asset URL pattern | Safe Cache-Control | Why |
|---|---|---|
Inter-Regular.e3b0c442.woff2 (hashed name) | public, max-age=31536000, immutable | The URL is unique per content — it can never go stale, so cache it for a year |
Inter-Regular.woff2?v=e3b0c442 (query) | public, max-age=31536000 (only if CDN keys on query) | Risky — drop immutable; if the CDN ignores the query you'll pin the wrong bytes for a year |
Inter-Regular.woff2 (no hash) | public, max-age=604800 + revalidate | No content signal in the URL, so you must allow weekly revalidation |
Cookbook
Copy-pasteable patterns. The hash values are illustrative; run your font through the Fingerprinter for the real cache_busted_filename and sri_attribute.
Filename hashing + immutable (the recommended pattern)
ExampleRename the deployed file to cache_busted_filename, reference it in CSS, and cache it for a year. The next version ships under a new name automatically.
# deployed asset
/fonts/Inter-Regular.e3b0c442.woff2
# response header on that file
Cache-Control: public, max-age=31536000, immutable
/* CSS */
@font-face {
font-family: "Inter";
font-display: swap;
src: url("/fonts/Inter-Regular.e3b0c442.woff2") format("woff2");
}Filename hashing + SRI together (defence in depth)
ExampleCombine both: the hashed filename handles cache invalidation, the SRI attribute handles tamper detection. Note crossorigin is mandatory for SRI on fonts.
<link rel="preload" as="font" type="font/woff2" crossorigin
href="/fonts/Inter-Regular.e3b0c442.woff2"
integrity="sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=">The query-string trap, made concrete
ExampleWhy ?v= is fragile: on a CDN that strips the query from the cache key, both URLs resolve to the same cached object and v2 never reaches users.
v1 deploy: /fonts/Inter.woff2?v=e3b0c442 v2 deploy: /fonts/Inter.woff2?v=9f86d081 CDN cache key (query stripped): /fonts/Inter.woff2 → both versions map to ONE cache entry → returning visitors keep getting v1 until the TTL expires Fix: use the hashed FILENAME, which the CDN cannot collapse.
Let the bundler own it, fingerprint to verify
ExampleIn a real build, Vite/Webpack emit [contenthash] filenames for you. Use the Fingerprinter to spot-check that the emitted hash matches the source bytes you expect.
// vite.config.ts — content hash in the output name
export default {
build: {
rollupOptions: {
output: { assetFileNames: "assets/[name].[hash][extname]" }
}
}
};
// then drop the source font into the Fingerprinter and confirm
// short_hash lines up with what landed in /assetsSelf-hosted Google Fonts with cache-busting
ExampleAfter migrating off the Google CDN, you own the filenames. Generate the @font-face CSS, then fingerprint each downloaded woff2 to produce immutable hashed names.
# 1. generate the @font-face CSS for self-hosting: # /font-tools/google-fonts-css-generator # 2. fingerprint each downloaded woff2 → cache_busted_filename # 3. deploy hashed names + Cache-Control: ...immutable Inter-Latin-400.e3b0c442.woff2 (max-age=31536000, immutable) Inter-Latin-700.9f86d081.woff2 (max-age=31536000, immutable)
Edge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
Query string stripped from the CDN cache key
Stale-font failureCloudflare and several other CDNs can be configured to ignore query strings in the cache key (or do so by default for some asset types). font.woff2?v=1 and font.woff2?v=2 then collapse to one cached object, and your font update silently never ships. This is the single most common cache-busting bug for fonts. Use the hashed filename (cache_busted_filename) instead — a different filename cannot be collapsed.
SRI without crossorigin
CORS-blocked failureAn integrity attribute on a font preload requires crossorigin (use the bare crossorigin keyword, which sends crossorigin=anonymous). Without it the browser refuses to apply the integrity check and the font fails CORS. The Fingerprinter emits the integrity="…" string only — you must add crossorigin yourself on the <link>.
SRI hash doesn't match after a re-deploy
Integrity check failsIf you re-build the font (even a no-op editor re-save that bumps the head timestamp) but forget to update the integrity attribute, the browser computes a different hash and refuses the font — text falls back to the system stack. SRI is intentionally strict. Re-fingerprint and update both the filename and the sri_attribute on every byte change.
immutable header on a non-hashed filename
Permanent stale riskCache-Control: immutable tells the browser to never revalidate for the max-age window. Put it on font.woff2 (no hash in the name) and a content change can leave returning visitors stuck on the old bytes for up to a year with no way to recover short of a hard refresh. Only send immutable on content-hashed filenames.
Short-hash collision in a huge font library
Vanishingly rareshort_hash is 8 hex chars = 32 bits. Across a few dozen fonts the collision probability is negligible, but a library with tens of thousands of distinct font files could in principle see two share a short_hash. If you operate at that scale, use a longer slice of sha256 for filenames or rely on your bundler's longer [contenthash].
Service worker caches the old font name
Stale until SW updateA service worker with a precache manifest pins assets by URL. If the SW manifest isn't regenerated with the new hashed filename, the SW keeps serving the old font from its own cache even after the CDN updates. Cache-busting at the CDN layer doesn't help if a SW sits in front of it — regenerate the precache manifest on every build.
format() hint is not validated against the bytes
By designThe format("woff2") hint in @font-face src only tells the browser which entries to skip downloading; it does not verify the bytes. A hashed filename keeps the right bytes flowing, but if you point a .woff2-named URL at TTF bytes the font still fails to parse. Cache-busting guarantees freshness, not correctness — confirm the format with font-format-identifier.
Preload with a stale integrity but live href
Double fetch / wasteIf your preload <link> and your @font-face reference different hashed filenames (e.g. you updated one but not the other), the browser preloads one file and then fetches a different one — the preload is wasted and the console warns the preloaded resource was not used. Keep the preload href, the CSS src, and the integrity all driven from the same fingerprint run.
Frequently asked questions
Why is filename hashing better than a query string?
A content-hashed filename (Inter-Regular.e3b0c442.woff2) is a genuinely different URL per version, so every cache layer — browser, CDN, proxy — treats it as a new resource and you get guaranteed invalidation plus safe immutable caching. A query string (Inter-Regular.woff2?v=…) reuses the same path, and many CDNs drop the query from the cache key, collapsing all versions to one stale object. For static font assets, filename hashing is the robust default.
What does the Fingerprinter give me for cache-busting?
Two of its four output fields: cache_busted_filename (<stem>.<short8>.<ext>) for filename hashing, and sri_attribute for integrity verification. There are no options — both are always emitted. Use the filename for cache invalidation and the SRI attribute for tamper detection; they're complementary, not alternatives.
Should I always add SRI to my fonts?
Add it when fonts are served from a third-party origin (a font CDN, a shared asset bucket) where a tampered or swapped file is a real risk — SRI then catches the swap before the browser uses the font. For first-party fonts on your own HTTPS origin, SRI's marginal benefit rarely justifies the rebuild discipline of keeping the hash in sync on every change. It is a security control, never a cache strategy.
Can I use the same hash for the filename and the SRI?
They come from the same SHA-256 digest but in different encodings. The filename uses the 8-char hex short_hash; the SRI uses the full digest base64-encoded. The Fingerprinter emits both from one run, so they always describe the same bytes — just don't try to put the base64 in the filename or the hex in the integrity attribute.
What Cache-Control should I send on a hashed font?
Cache-Control: public, max-age=31536000, immutable. Because the filename changes when the content changes, the URL can never serve stale bytes, so a one-year immutable cache is safe and gives returning visitors instant zero-byte cache hits. Only use immutable on content-hashed filenames — never on a bare font.woff2.
Do bundlers already do this for me?
Yes — Webpack [contenthash], Vite/Rollup [hash], and Next.js asset hashing all emit content-hashed filenames automatically and update the CSS references. The Fingerprinter is for the cases the bundler doesn't own: a font dropped straight into /public, a hand-rolled deploy, or verifying that the hash your bundler emitted matches the source bytes you expect.
Why did my font not update after I shipped a new version?
Almost always one of: (1) you used a query string and the CDN stripped it from the cache key; (2) you reused the same filename with an immutable header so the browser never revalidated; (3) a service worker is serving the old file from its precache. The fix for all three is content-hashed filenames — a new filename forces a fresh fetch through every cache layer.
Does cache-busting affect the font's first-load performance?
No — first-time visitors fetch the font once regardless of the URL pattern. Cache-busting only governs what happens on subsequent visits and after a version change. To improve first paint, preload the critical font (<link rel="preload" as="font">) and use font-display: swap — see preload-tag-builder and font-display-strategy.
How do I keep the preload, CSS, and integrity in sync?
Drive all three from a single fingerprint run per font. Generate cache_busted_filename once, use it as the href in the preload and the src in @font-face, and use sri_attribute for the integrity. In a build pipeline, emit them together so they can never drift — see the build-script guide.
Is 8 characters enough for the filename hash?
For a normal project, yes — 8 hex chars (32 bits) makes collisions across your handful of font files effectively impossible. If you host an enormous font library (tens of thousands of distinct files) the birthday bound gets uncomfortable; use a longer slice of the full sha256 or your bundler's longer hash. For integrity, always use the full hash via sri_attribute.
Does generating the hash upload my font?
No. The SHA-256 is computed in your browser with crypto.subtle.digest; the result panel shows 0 bytes uploaded. Your font — including unreleased or licensed brand faces — never reaches a server to be cache-busted. On paid tiers it can route to the local @jadapps/runner on 127.0.0.1, still entirely on your machine.
What about HTTP/2 push or 103 Early Hints for fonts?
Those are delivery-timing optimisations and are independent of cache-busting — they get the bytes to the browser sooner but don't change how the cache decides freshness. Content-hashed filenames still apply on top: the early-hinted/preloaded URL should be the hashed one so it benefits from immutable caching on repeat visits.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.