Skip to content

Commit 7cf6511

Browse files
committed
Add No-Vary-Search post
1 parent a133056 commit 7cf6511

1 file changed

Lines changed: 292 additions & 0 deletions

File tree

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
---
2+
layout: post
3+
title: "Better Browser Caching with No-Vary-Search"
4+
date: 2026-05-07 21:31:40
5+
categories: Web Development
6+
main: ""
7+
meta: "No-Vary-Search lets HTTP caches ignore irrelevant query parameters such as UTM tags, while still keeping meaningful ones like product variants in the cache key."
8+
---
9+
10+
I’ve [written](/2019/03/cache-control-for-civilians/),
11+
[spoken](https://speakerdeck.com/csswizardry/cache-rules-everything), and
12+
generally [gone on](/2025/03/why-do-we-have-a-cache-control-request-header/) [at
13+
length](/2024/08/cache-grab-how-much-are-you-leaving-on-the-table/) [about
14+
caching](/2023/10/the-three-c-concatenate-compress-cache/) for years now, but
15+
a newer addition to the conversation is `No-Vary-Search`: an HTTP response
16+
header that helps us solve a surprisingly common problem with cache keys in HTTP
17+
cache (or _browser cache_).
18+
19+
The short version is, URLs that are materially the same often fail to reuse the
20+
same cached response simply because their query strings differ. Sometimes that
21+
difference matters to the content and, therefore, the end user. For example:
22+
23+
* `?colour=red`
24+
* `?colour=blue`
25+
26+
And sometimes, it really doesn’t matter at all:
27+
28+
* `?utm_source=google`
29+
* `?utm_source=chatgpt`
30+
31+
The former are very likely different pages, or at least pages that ought to
32+
produce different content. We would not want them cached under the same key.
33+
34+
The latter should, in almost every sane setup, return the exact same HTML. They
35+
are the same page, just with different tracking baggage attached.
36+
37+
And yet, to an HTTP cache, different query strings traditionally equate to
38+
completely different URLs, which means different cache entries. That is wasteful.
39+
40+
## The Problem We’re Solving
41+
42+
By default, caches are cautious. If the URL differs, the cache key differs, and,
43+
in a cautious world, that is usually the right thing to do.
44+
45+
This is why:
46+
47+
* `/products/shoes?colour=red`
48+
* `/products/shoes?colour=blue`
49+
50+
…should remain distinct. The query parameter materially changes the content of
51+
the page.
52+
53+
But this also means the cache will usually treat these as distinct pages,
54+
too—even if all three return byte-for-byte identical HTML.
55+
56+
* `/sale?utm_source=google`
57+
* `/sale?utm_source=chatgpt`
58+
* `/sale?utm_source=newsletter`
59+
60+
At best, that means wasted cache space; at worst, it means unnecessary trips
61+
across the network because the browser cannot reuse a perfectly good response
62+
that it already has stored.
63+
64+
**This is exactly what `No-Vary-Search` addresses.**
65+
66+
## What `No-Vary-Search` Does
67+
68+
[`No-Vary-Search`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/No-Vary-Search)
69+
is a response header that tells the cache how to treat query parameters when
70+
matching a URL to an existing cache entry.
71+
72+
In other words, it lets the server say, via a header, <q>these search parameters
73+
do not meaningfully change the response, so do not let them fragment the
74+
cache</q>.
75+
76+
This is a little reminiscent of the well known
77+
[`Vary`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Vary)
78+
header, but aimed squarely at URL search parameters rather than request headers.
79+
80+
That distinction matters. `Vary` says <q>this response depends on
81+
`Accept-Language`</q> or <q>this response depends on `Accept-Encoding`</q>, for
82+
example. `No-Vary-Search` says <q>this response does _not_ depend on
83+
`utm_source`</q> or <q>the order of these parameters should not matter</q>, and
84+
so on.
85+
86+
If you know that certain parameters are irrelevant to the response body, you can
87+
tell the cache to ignore them, effectively allow- or blocklisting them for cache
88+
key purposes.
89+
90+
## A Simple Example
91+
92+
Imagine a landing page that is heavily used in campaigns, ads, email, and
93+
social posts:
94+
95+
* `/offer?utm_source=google`
96+
* `/offer?utm_source=chatgpt`
97+
* `/offer?utm_source=linkedin`
98+
99+
If those all return the same page, you can tell the cache to ignore
100+
`utm_source`:
101+
102+
```http
103+
No-Vary-Search: params=("utm_source")
104+
```
105+
106+
Now, as far as cache matching is concerned, those URLs may all reuse the same
107+
stored response. They will no longer be treated as separate cache entries just
108+
because of the different `utm_source` values. Much more effective reuse of
109+
cached content.
110+
111+
That is the crucial thing to understand: `No-Vary-Search` is not changing the
112+
URL, and it is not rewriting requests. It is changing how cache matching treats
113+
the differing query string.
114+
115+
## `No-Vary-Search` Syntaxes
116+
117+
The header has a few useful, different forms.
118+
119+
### Ignore Specific Parameters
120+
121+
This is the form I suspect most people will use most often:
122+
123+
```http
124+
No-Vary-Search: params=("utm_source" "utm_medium" "utm_campaign" "fbclid" "gclid")
125+
```
126+
127+
This says: if the only differences between two URLs are those parameters, treat
128+
them as the same cache key.
129+
130+
This is ideal for analytics and campaign tagging, where the query string is
131+
useful to _you_ but should not change the response for the user. These standard
132+
tracking and social parameters could probably be safely applied to most, if not
133+
all, sites.
134+
135+
### Ignore All Parameters
136+
137+
If your page genuinely does not vary by query string at all, you can be much
138+
broader:
139+
140+
```http
141+
No-Vary-Search: params
142+
```
143+
144+
That is the boolean form of `params`, and it tells the cache to ignore _all_
145+
search parameters for matching purposes. This works perfectly for my site which
146+
has zero back end, and thus cannot possibly vary by query string.
147+
148+
This is obviously powerful, but also the easiest way to shoot yourself in the
149+
foot, so only use it if you really mean it.
150+
151+
### Ignore Everything _Except_ Certain Parameters
152+
153+
Sometimes the inverse is easier to express. Perhaps most parameters are
154+
irrelevant, but a small number genuinely change the response:
155+
156+
```http
157+
No-Vary-Search: params, except=("colour" "size")
158+
```
159+
160+
This says: ignore all query parameters _except_ `colour` and `size`.
161+
162+
That would be a decent fit for a page where:
163+
164+
* `utm_*` tags do not matter,
165+
* client-side sorting/filtering or tracking parameters do not matter (remember,
166+
the HTML itself doesn’t change) but,
167+
* product variants (`?colour`, `?size`) do.
168+
169+
In that world:
170+
171+
* `/products/shoes?utm_source=google&colour=red`
172+
* `/products/shoes?utm_source=chatgpt&colour=red`
173+
* `/products/shoes?colour=red&sort=price`
174+
175+
could share a cache entry, but:
176+
177+
* `/products/shoes?colour=red`
178+
* `/products/shoes?colour=blue`
179+
180+
should not.
181+
182+
### Ignore Parameter Order
183+
184+
Sometimes the parameters themselves matter, but their order does not:
185+
186+
* `/search?q=shoes&sort=price`
187+
* `/search?sort=price&q=shoes`
188+
189+
These should usually be treated as equivalent. For that, there is `key-order`:
190+
191+
```http
192+
No-Vary-Search: key-order
193+
```
194+
195+
That tells the cache not to create separate entries just because the same
196+
parameters arrived, only in a different order.
197+
198+
### Combining `No-Vary-Search` Rules
199+
200+
You can combine directives:
201+
202+
```http
203+
No-Vary-Search: key-order, params, except=("colour" "size")
204+
```
205+
206+
That tells the cache:
207+
208+
* parameter order does not matter, and
209+
* all parameters may be ignored except `colour` and `size`.
210+
211+
This means the three following URLs could all share a cache entry:
212+
213+
* `/products/shoes?utm_source=google&colour=red`
214+
* `/products/shoes?colour=red&sort=price`
215+
* `/products/shoes?sort=price&colour=red`
216+
217+
That is probably the most expressive form, and in real systems it may prove the
218+
most useful. This is phenomenally powerful.
219+
220+
## A Small Syntax Gotcha
221+
222+
`No-Vary-Search` uses [Structured
223+
Fields](https://www.rfc-editor.org/rfc/rfc8941) syntax, so the parameter lists
224+
are space-separated quoted strings:
225+
226+
```http
227+
No-Vary-Search: params=("utm_source" "utm_medium" "gclid")
228+
```
229+
230+
…not comma-separated values that you may be used to in most other places.
231+
232+
That is a small detail, but one worth being aware of.
233+
234+
## A Small Debugging Gotcha
235+
236+
Note that this also creates a slightly unusual debugging scenario. A very common
237+
way to force what we assume will be a fresh trip to the server is to throw
238+
a random search parameter on the end of the URL. I’m sure we’ve all done
239+
something like this before:
240+
241+
* `/?foo`
242+
* `/?test`
243+
* `/?asdf`
244+
245+
Ordinarily, that gives us a different URL and therefore a different cache key.
246+
But if the main document is using `No-Vary-Search`, that assumption may no
247+
longer hold. Appending search params may not bypass cache for this document
248+
because the cache has explicitly been told those parameters do not matter.
249+
250+
Honestly, I would love DevTools to surface this more clearly. Something akin to
251+
the existing ⚠️ iconography in the _Network_ panel’s title, would be really
252+
helpful here: not because anything is wrong per se, but because the browser may
253+
be doing something surprising unless you know to look for the `No-Vary-Search`
254+
header.
255+
256+
## Use It Carefully
257+
258+
This header is only as good as the assumptions behind it: if two URLs really do
259+
return meaningfully different content, then they _need_ different cache entries.
260+
I’d rather be served the correct page a little more slowly than the wrong page
261+
quickly.
262+
263+
This means `No-Vary-Search` is best suited to parameters that are:
264+
265+
* purely analytical;
266+
* purely presentational on the client side, or;
267+
* otherwise irrelevant to the server-rendered response.
268+
269+
If a parameter affects the HTML, do not ignore it.
270+
271+
It is also worth noting that, as of **7 May 2026**, this is still an
272+
experimental feature and support is not yet universal, so I would treat it as a
273+
progressive enhancement rather than a foundational part of your caching
274+
strategy.
275+
276+
## A Nice Fit for Messy Real-World URLs
277+
278+
What I like about `No-Vary-Search` is that it acknowledges how the web actually
279+
works.
280+
281+
URLs pick up baggage. Marketing tags get appended. Tracking parameters get
282+
added. Client-side state leaks into the address bar. Two URLs that are
283+
materially the same page often arrive wearing different coats.
284+
285+
Historically, caches had to treat those as entirely separate keys.
286+
287+
`No-Vary-Search` gives us a way to be a little more honest. If the response is
288+
the same, we can say so. And if only certain parameters matter, we can say that
289+
too.
290+
291+
For teams who care about getting more out of HTTP cache, that is a very welcome
292+
addition.

0 commit comments

Comments
 (0)