yum upgrades for production use, this is the repository for you.
Active subscription is required.
📅 Updated: June 3, 2026 (Originally published: November 12, 2016)

If you strip query parameters with Varnish before the cache lookup, every campaign or session-tagged URL collapses onto the same cached object instead of inflating your cache and starving the hit rate.
There are often cases when you need Varnish to cache the page whether it contains query parameters or not. The most common example is when Google (Ads, Analytics, etc.) appends tracking parameters such as ?gclid= or ?utm_source= to your URLs.
If left untouched, Varnish will hold a separate cache entry for every distinct URL, even though every variant returns the exact same page. The result is a much lower cache hit rate, more origin requests, and slower responses for users coming in from campaigns.
The solution is to strip query parameters with Varnish in vcl_recv before the lookup, so all the variants collapse into a single cacheable object. Below are two recipes — drop specific parameters, or drop all of them for a given route — using either plain VCL or the vmod-querystring module.
How to strip marketing and analytics query parameters
The classic case: drop gclid and any utm_* parameter so campaign URLs collapse onto the canonical page. Add this to your vcl_recv (between sub vcl_recv { and the closing }):
if (req.url ~ "(\?|&)(gclid|utm_[a-z]+)=") {
set req.url = regsuball(req.url, "(gclid|utm_[a-z]+)=[-_A-z0-9+()%.]+&?", "");
# remove trailing question mark and ampersand
set req.url = regsub(req.url, "[?|&]+$", "");
}
You can test the matching regex by opening this regex101 link. It handles edge cases including values that contain round brackets.
The campaign variables stripped here — utm_source, utm_medium, utm_campaign, gclid, and friends — are only needed by the JavaScript that runs on the page (Google Analytics reads them directly from the original Referer / document.location before Varnish ever sees the request). Dropping them server-side does not break attribution; it just lets Varnish serve every visitor the same cached HTML.
How to strip ALL query parameters for specific routes
Sometimes you have a route where every parameter is irrelevant to the response body — feeds, static-ish landing pages, generated PDFs, sitemaps, or images that ignore the querystring entirely. For those routes you can wipe the whole querystring wholesale, instead of enumerating individual parameter names:
sub vcl_recv {
# Drop ALL query parameters from these routes only
if (req.url ~ "^/feed(/|\?|$)" ||
req.url ~ "^/wp-sitemap" ||
req.url ~ "^/some-landing-page(/|\?|$)") {
set req.url = regsub(req.url, "\?.*$", "");
}
}
The regsub(req.url, "\?.*$", "") substitution drops everything from the ? onwards, leaving you with a clean path that Varnish can cache as a single object.
Warning — never apply this site-wide. Wiping every query string globally will break search forms (
?s=...), pagination (?page=2), faceted product listings, REST endpoints (/wp-json/...?_fields=...), and any URL where the parameter actually changes the response. Always gate the rule on a specific path prefix so only known-cache-irrelevant routes are affected.
If you also want to remove the query string before the cache lookup but forward it to the backend, save it first:
sub vcl_recv {
if (req.url ~ "^/track(/|\?|$)") {
set req.http.X-Original-Query = regsub(req.url, "^[^?]*\??", "");
set req.url = regsub(req.url, "\?.*$", "");
}
}
sub vcl_backend_fetch {
if (bereq.http.X-Original-Query) {
set bereq.url = bereq.url + "?" + bereq.http.X-Original-Query;
unset bereq.http.X-Original-Query;
}
}
This pattern is useful when the origin needs to see the parameters (for logging or A/B routing) but they should not factor into the cache key.
Cleaner VCL with vmod-querystring
For anything beyond a handful of parameter names, the vmod-querystring module gives you a much cleaner way to strip query parameters with Varnish. It is the same approach the Accelerate WordPress with Varnish Cache guide uses, and it has a smaller memory footprint than chained regsuball calls — particularly on long URLs.
Drop specific marketing parameters via vmod-querystring
import std;
import querystring;
sub vcl_init {
new tracking_params_filter = querystring.filter();
tracking_params_filter.add_string("gclid");
tracking_params_filter.add_glob("utm_*"); # Google Analytics
tracking_params_filter.add_glob("fbclid"); # Facebook click ID
tracking_params_filter.add_glob("mc_*"); # Mailchimp
tracking_params_filter.add_glob("ref"); # generic referrer tag
}
sub vcl_recv {
set req.url = tracking_params_filter.apply(req.url);
}
Because the filter is a vcl_init object, parameter lookups are O(1) and the configuration is much easier to audit and extend. If you need a parameter name that is more easily expressed as a regex than as a glob, the VMOD also exposes .add_regex(...).
Drop ALL parameters via vmod-querystring
To remove the entire querystring on a route, use querystring.clean():
sub vcl_recv {
if (req.url ~ "^/feed(/|\?|$)" ||
req.url ~ "^/wp-sitemap") {
set req.url = querystring.clean(req.url);
}
}
querystring.clean() removes every parameter and tidies up the trailing ? for you. It is the VMOD equivalent of the regsub(req.url, "\?.*$", "") recipe above, but much easier to reason about in code review.
You can also keep an explicit allow-list of parameters and drop everything else with querystring.filter() in keep mode — useful when you want to cache by ?page=N only and ignore everything else:
import querystring;
sub vcl_init {
new pagination_only = querystring.filter(mode = keep);
pagination_only.add_string("page");
}
sub vcl_recv {
if (req.url ~ "^/blog(/|\?|$)") {
set req.url = pagination_only.apply(req.url);
}
}
Installing vmod-querystring
The GetPageSpeed extras repository ships vmod-querystring for every supported combination of Varnish and RHEL-family distro. Install the release RPM once, then the VMOD itself:
Varnish 6.0 LTS — RHEL/AlmaLinux/Rocky 8
sudo dnf install -y https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install -y vmod-querystring
Varnish 6.0 LTS — RHEL/AlmaLinux/Rocky 9 or 10
sudo dnf install -y https://extras.getpagespeed.com/release-latest.rpm
sudo dnf config-manager --enable getpagespeed-extras-varnish60
sudo dnf install -y vmod-querystring
Varnish 7.x — RHEL/AlmaLinux/Rocky 8, 9, or 10
sudo dnf install -y https://extras.getpagespeed.com/release-latest.rpm
sudo dnf config-manager --enable getpagespeed-extras-varnish70
sudo dnf install -y vmod-querystring
RHEL 7 / CentOS 7? Those distros are EOL (June 2024) and we strongly recommend migrating off. If you still need to run there, the legacy commands in our original RHEL 7 / Varnish 4.x installation notes still apply, but we no longer publish new builds for that target.
After installing, reload Varnish (sudo systemctl reload varnish) and verify with man vmod_querystring for the full reference.
Measuring the cache-hit-rate impact
Cleaning up the URL before lookup is only worth doing if you can prove it raised your hit rate. Two built-in tools make this easy.
varnishstat exposes the running counters. The two you care about are MAIN.cache_hit and MAIN.cache_miss. Take a baseline before deploying the VCL change:
varnishstat -1 -f MAIN.cache_hit -f MAIN.cache_miss
Reload Varnish with the new VCL (sudo systemctl reload varnish), let your traffic warm the cache for fifteen minutes, then sample again. Hit ratio hit / (hit + miss) typically jumps by single-digit percentage points on a campaign-heavy site, and substantially more on a site that ran into route-specific querystring fragmentation (sitemap probes, feed readers polling with cache-busters).
varnishlog lets you confirm individual requests look right after rewriting. Tail it scoped to the route you changed:
varnishlog -g request -q 'ReqURL ~ "^/feed"'
You should see the ReqURL field with the parameter already stripped, and a Hit or Miss tag a few lines below — never a HitMiss (which would indicate the object went through hit-for-miss because of an uncacheable Vary).
For a deeper distribution view, varnishhist -p hit plots a histogram of hit vs. miss latency in real time — a good way to spot a route that you think you cleaned but is still missing because of a stray Set-Cookie upstream.
Troubleshooting common pitfalls
A few traps catch most people the first time they strip query parameters with Varnish in production:
- Backend still receives the original URL. You rewrote
req.urlinvcl_recv, but a hand-writtenvcl_backend_fetchis rebuilding the original querystring from a saved header. Check that you are not leaking the original parameters throughbereq.http.*unless you specifically want to forward them. - Some campaigns still create multiple cache entries. Watch out for parameter name casing (
?UTM_source=...) or trailing whitespace from copy-paste. Regex anchors and thevmod-querystringglob are both case-sensitive by default — normalize withstd.tolower(req.url)before matching if your CMS produces mixed case. - Hit rate did not move at all. Either your
Varyresponse header is fragmenting cache by something else (User-Agent, Accept-Encoding without normalization), or your CMS is sendingCache-Control: privateon the cleaned URL.varnishlogwill show aBerespHeader Cache-Control: privateline that explains it immediately.
Which pattern should I use?
| Use case | Recommended pattern |
|---|---|
| A handful of campaign parameters across the whole site | Pure VCL regsuball — fine, no extra dependency |
| Many parameter names, or you want a clean allow-list | vmod-querystring with querystring.filter() |
| Specific routes where the querystring is always irrelevant | Gated regsub(req.url, "\?.*$", "") or querystring.clean() |
| Cache by one or two parameters, drop the rest | querystring.filter(mode = keep) |
Whichever you choose, run varnishlog -g request -q "ReqURL" after the change to confirm the rewritten URL reaches the backend with the querystring you expect — and watch your cache hit rate climb.
Related reading
- Accelerate WordPress with Varnish Cache — the full WordPress-aware VCL we recommend, which already wires in
vmod-querystring. - Magento 2 Varnish Multi-Store VCL for a Single Server — applying the same patterns to a multi-site Magento setup.

Tim
Thank you very much for this VCL adaptation to strip out utm tags from urls. I added \%\. to strip out also % and . as these characters were in many of my utms
Danila Vershinin
Hi Tim.
Thanks for your input. I’ve added the missing bits to regex.
Actually, I think escaping is not needed in “character class” part of regex, so it has been removed now.
laura b
Hi there, Thanks for this post. I think my question is related. I seem to have the issue where a cached version of the page with the gclid or fbclid parameters is making it’s way into things like the pagination or filters of our site and the marketing tracking is being appended to urls within the site – the hard code doesn’t have the tracking perams, they’re being applied through serving a cached version which has them. If we ensure that varnish strips these out first would it solve our problem as nothing with a marketing utm or click id will be able to be cached in the first place to be served up to general users who came via another source?
In case my example isn’t too clear: I come to the site direct with a clean url. I click on ‘dresses’, I use the link at the bottom of the first page to visit “/p=2”. The link that I click to see page two applies a gclid for a ‘black dresses’ generic google ad keyword campaign. If I buy the sale attributes to that keyword even though I came direct.
It’s a big problem for us as it’s inflating ads campaigns and making optimisation impossible guesswork.
So grateful to know advice on how to prevent this.
Danila Vershinin
There’s obviously a problem in your framework, in how it constructs URLs for pagination. In all likelihood, it considers
$_SERVER['REQUEST_URI'](if we’re talking PHP) while constructing links, whereas it shouldn’t.Sure enough, applying the stripping as above in Varnish will resolve the glcid issue you’re having. But framework should be fixed as well, as it likely will construct “bad” URLs from any arbitrary parameters.