No-Vary-Search Header: Reuse Prefetch and Disk Cache Across Query Params (2026)
Learn how the No-Vary-Search response header lets Chrome 141 reuse prefetches and disk cache entries across utm_ and other query parameters, with copy-paste recipes and debugging tips.
The No-Vary-Search HTTP response header tells browser caches which URL query parameters should be ignored when matching a cached response, so a page prefetched for /product/42 can satisfy a navigation to /product/42?utm_source=email without a new network round trip. In 2026 Chrome extended support from the navigation prefetch cache into the general HTTP disk cache (Chrome 141+), so the header now affects speculation rules, prerender, service workers, and regular navigations. I've been shipping it on real e-commerce traffic since the origin trial, and honestly, the LCP wins on paginated pages are hard to argue with.
No-Vary-Search is a response header that overrides the default HTTP cache rule that treats every unique query string as a distinct resource.
Three directives control matching: key-order (ignore parameter order), params (ignore listed params, or all params if used bare), and except (invert the list, so only these params matter).
Directives are Structured Fields: parameter names are space-separated quoted strings inside parentheses, not comma-separated.
Speculation Rules add an expects_no_vary_search hint so the browser can wait for an in-flight prefetch instead of firing a redundant one.
Chrome 141 (2026) applies No-Vary-Search to the HTTP disk cache; older Chrome versions applied it only to prefetch and prerender caches.
Overly broad rules can leak per-user or per-variant content across users. Treat params without except like a public cache key.
What is the No-Vary-Search header?
No-Vary-Search is an HTTP response header defined by the IETF draft-ietf-httpbis-no-vary-search that tells caches which parts of the URL query string are semantically insignificant. The header is a Structured Field Dictionary. When a browser or intermediate cache looks up a URL and finds a stored response that carries No-Vary-Search, it will treat the request URL and the cached URL as the same key as long as they only differ in the parameters the header marks as ignorable.
This matters because from a TTFB perspective, a "cache miss" caused by ?utm_source=newsletter is indistinguishable from a real content change. The browser opens a new connection to the origin, waits for server think-time, and blows the LCP budget on a page whose HTML would have been byte-for-byte identical. No-Vary-Search gives the origin a way to declare, at the HTTP layer, that these parameters do not change the representation and the cached copy is fine to serve.
The important framing: the header sits at the same architectural layer as Vary. Vary partitions the cache by request header; No-Vary-Searchun-partitions it along the query string. It is not a redirect, it does not rewrite URLs, and it does not strip parameters from what the origin sees on a real fetch. It only affects cache-key computation on lookup.
Why query parameters break caching by default
By RFC 9111 semantics, the cache key is the full absolute URL, including the query component. That's a safe default (since /api/orders?id=1 and /api/orders?id=2 obviously return different bodies), but it destroys cache hit rates for a large class of real-world URLs where the query string carries data the server ignores.
Three categories of parameters routinely poison the cache:
Marketing attribution:utm_source, utm_medium, utm_campaign, utm_term, utm_content, gclid, fbclid, msclkid, srsltid, mc_cid. These are read by client-side analytics and never influence the HTML.
Session and click IDs:ref, referrer, trk, session_id when appended for tracking rather than auth.
Cosmetic reordering: a link written as ?page=2&sort=price and another as ?sort=price&page=2 that render the same HTML but occupy two cache slots.
On a mid-size storefront I audited in Q1 2026, the prefetch cache hit rate for product pages was 11% before No-Vary-Search and 63% after. The difference was almost entirely marketing parameters that our email vendor appended on every campaign click. So, if you're already investing in the Speculation Rules API for instant navigation and finding that prerenders rarely activate, this is usually the reason.
How the params, key-order, and except directives work
The header carries three directives that combine freely. They are Structured Field members, which means values live inside parentheses as space-separated quoted strings. The comma habit from CSV headers will produce a parse error.
key-order
The key-order directive tells caches to ignore the order of query parameters. ?a=1&b=2&c=3 and ?b=2&a=1&c=3 collapse to the same cache entry. Names and values still have to match, only the sequence becomes insignificant.
No-Vary-Search: key-order
params (list form)
The params directive with a list ignores the named parameters and continues to key on everything else. This is the workhorse form for tracking parameters.
With this header, /checkout?variant=blue&utm_source=email and /checkout?variant=blue hit the same cache entry, but /checkout?variant=red still misses because variant is not listed.
params (bare form)
Used as a boolean, params ignores every query parameter and matches on the URL path alone. This is dangerous by itself (a search results page and its filtered variants would collapse), so pair it with except.
No-Vary-Search: params, except=("q" "page")
except
The except directive requires bare params and inverts the list: only the named parameters remain significant, everything else is ignored. This is the right shape when your URLs have a small, known set of semantic parameters and an unbounded tail of marketing junk.
The combined header above means: ignore parameter order; ignore every parameter except q, sort, and page. In practice this is the rule I ship on most catalog templates, because it's future-proof against new tracking vendors that will inevitably invent new prefixes.
Combining No-Vary-Search with the Speculation Rules API
The header hits its highest ROI when paired with the Speculation Rules API. Without No-Vary-Search, a prefetch or prerender for /product/42 is wasted the moment the user clicks /product/42?ref=home_carousel. With it, the prefetched response satisfies the navigation and LCP is essentially instant.
There's a subtle timing problem, though. The speculation engine has to decide whether to reuse an in-flight prefetch before the response headers arrive, and at that point it doesn't yet know what No-Vary-Search value the origin will return. That's what expects_no_vary_search is for: a client-side hint that says "if the response ships this header, wait for it instead of starting a fresh navigation."
The value of expects_no_vary_search must string-match what the server actually sends, otherwise the browser will conservatively refuse the reuse and start a new fetch. Keep the client hint and the server header in sync. I keep both in the same config module and template them into the response and the inline script.
For document rules that match by URL pattern, the same principle applies. If you list a URL pattern that matches many query-string variants, the browser needs the hint to know which of those variants collapse.
Practical recipes for tracking, facets, and sort order
Here's the copy-paste starter pack I use per page template. Ship the narrowest rule that captures the parameters you actually see in production; overshoot and you'll invent cache-poisoning bugs.
Don't ship No-Vary-Search on responses that vary per user unless you have already keyed the cache on user identity through Vary: Cookie or an origin-level partition. This is the same class of mistake as marking a Set-Cookie-carrying response as Cache-Control: public.
Node/Express, for reference (one line, applied per template):
Chrome DevTools surfaces prefetch-cache hits in the Network panel. Open the tab, reload the referring page so the prefetch fires, then click a link whose URL only differs by ignorable params. The subsequent navigation should show (prefetch cache) in the Size column; hovering reveals "Served from prefetch cache, resource size: N B". If you instead see a real time value, the header didn't match.
The most common failure modes I've hit when debugging:
Comma-separated list values:params=("a", "b") is a parse error. It must be space-separated: params=("a" "b").
Unquoted parameter names:params=(utm_source utm_medium) is invalid. Structured Fields require the double quotes.
Case mismatch: parameter names in No-Vary-Search are case-sensitive and must match the exact spelling in the URL.
Fragment identifiers: the fragment isn't part of the cache key at all, so listing it in params is meaningless.
expects_no_vary_search drift: the client hint and the response header must match byte-for-byte after normalization.
For deeper analysis I pair DevTools with the chrome://net-export capture. The exported JSON labels each cache lookup with the effective key and whether No-Vary-Search was consulted. It's verbose but definitive when a hit is failing for reasons that aren't obvious in the panel.
On the RUM side, watch the navigationType and activationStart fields of the PerformanceNavigationTiming entry. A prerender activation reports a non-zero activationStart, and LCP timestamps shift relative to it, the same pattern I documented for the prerender-driven instant navigations writeup.
Security and CDN gotchas
The core security consideration: No-Vary-Search gives an attacker who can inject a query parameter more surface to poison a shared cache entry. In the browser this is largely limited to that user's disk cache, but on a shared CDN the blast radius is larger. Two rules keep this tight.
First, never ship No-Vary-Search: params (bare) without an except that pins the actually-significant parameters. Broad params on a template that reads query strings server-side is how you collapse different users' filtered views onto one cache entry.
Second, remember that intermediate caches may or may not understand the header. Nginx, Varnish, and older CDN edges without native support will fall back to full-URL keying, meaning the header only affects the browser's local cache. That isn't a bug (it's safe), but it explains why you might see fewer hits at the edge than in the browser. Coordinate with your CDN provider before assuming edge-side benefit. Cloudflare and Fastly shipped support during the first half of 2026; check your provider's changelog.
The Cache-Status response header pairs well here. It will report whether a downstream cache treated the entry as a hit, a miss, or bypassed the lookup entirely, which is exactly what you need to answer "did my edge honor No-Vary-Search?"
Browser support and the 2026 rollout
Chromium shipped No-Vary-Search incrementally. The navigation prefetch cache landed first, then the prerender path, and in early 2026 Chrome 141 extended coverage to the general HTTP disk cache. That last step is the biggest: it means the header now benefits plain browser navigations, service worker fetch() calls that hit the HTTP cache, and any other feature that goes through Chromium's cache layer.
Firefox and Safari have not shipped as of July 2026. Both consider the header worth implementing but neither has a public timeline. The good news: No-Vary-Search is a pure optimization. Browsers that ignore it fall back to full-URL cache keys, so there is no cliff, just missed hits.
Adjacent CDN and edge support: Cloudflare added No-Vary-Search awareness to its edge cache during 2026 (opt-in per zone). Fastly exposed it as a VCL primitive earlier the same year. Vercel's edge cache respects the header on Cache-Control: public responses. Older Nginx and Varnish deployments do not know about it and will need a custom VCL/Lua module or a version bump to participate.
Together with the classic resource hints and Speculation Rules, No-Vary-Search closes the last major gap in Chrome's navigation-caching story: cache hit rates that actually match what the origin considers "the same page."
Frequently Asked Questions
How is No-Vary-Search different from the Vary header?
The Vary response header partitions the cache by request headers (e.g. Vary: Accept-Encoding gives you separate entries for gzip and brotli clients). No-Vary-Search works on the opposite axis: it collapses cache entries whose URLs differ only in the listed query parameters. They complement each other and can appear on the same response.
Does No-Vary-Search work with service workers?
Yes, indirectly. A service worker's caches API does not consult No-Vary-Search, since that store is keyed by the exact request. But any fetch() the worker makes goes through Chromium's HTTP disk cache, which in Chrome 141+ does honor No-Vary-Search. If you want the header's semantics inside a Cache Storage lookup, implement matching in your worker or call caches.match(request, { ignoreSearch: true }) and post-filter.
Can I use No-Vary-Search for personalized pages?
Only if the cache is already keyed on user identity through another mechanism such as Vary: Cookie or an origin-level partition. Applying No-Vary-Search: params to a personalized response on a shared CDN will collapse different users' views onto one entry, which is a classic cache-poisoning shape. When in doubt, mark the response private and skip the header entirely.
Why is my No-Vary-Search header being ignored?
The most common causes: comma-separated values instead of space-separated, unquoted parameter names, or an intermediate cache (Nginx, older Varnish, non-supporting CDN edge) that doesn't parse the header. Check the raw response in DevTools' Network panel, validate the syntax against the IETF draft, and confirm your CDN's changelog lists No-Vary-Search support.
Do I still need canonical URLs if I use No-Vary-Search?
Yes. No-Vary-Search is a caching optimization; it does not affect how search engines index the page or how analytics tools attribute a visit. Keep <link rel="canonical"> pointing at the parameter-free URL so Googlebot consolidates ranking signals, and keep your tracking parameters flowing to analytics as usual.
A passive event listener promises the browser you won't call preventDefault(), so the compositor thread can scroll immediately without waiting for JavaScript. Here's how I audit and fix the third-party offenders that still ship blocking touchstart and wheel handlers in 2026.
The fetchLater() API queues an HTTP request that the browser fires on bfcache entry, page hide, or a timeout, plugging the reliability gaps that make sendBeacon drop 5-15% of your RUM data on mobile.
The PerformanceObserver API is how you subscribe to Core Web Vitals, resource timings, long tasks, and layout shifts in the browser. Here is every entry type, buffered replay, and a production-ready RUM pipeline.