Most arguments about URL parameters are conducted without numbers. Someone says faceted navigation is "eating crawl budget," someone else says Google handles duplicates fine, and nobody checks. Both positions can be true on the same site: Google does dedupe near-identical pages, but it dedupes after fetching them, so the crawl cost is paid before the duplicate is recognized.
The useful question isn't "are parameters bad." It's "what percentage of Googlebot's requests to my site in the last 30 days went to URLs that produced content I already served somewhere else?" That number is computable from your access logs plus one targeted crawl, and it turns a religious debate into a budget decision.
Why parameters multiply faster than anyone estimates
Filtered listings generate URLs combinatorially. Five filter dimensions with four values each, plus sort order and pagination, is not twenty URLs — it's the product of the selectable combinations, and that product climbs into the hundreds of thousands before anyone notices. Every combination that renders a 200 with a body is a crawlable, fetchable, schedulable URL.
Google treats each distinct URL string as a distinct resource for discovery and scheduling purposes. Canonicalization happens later, as a post-fetch clustering decision based on content, canonical hints, redirects, and internal linking. Google retired the URL Parameters tool in Search Console in 2022, stating it was rarely used and that its own parameter handling had improved. That removed the one explicit control you had. What's left is linking, robots.txt, canonicals, and response codes — all of which act at different points in the pipeline.
Google's own crawl budget documentation is clear that sites below roughly a few thousand URLs are usually crawled efficiently without intervention. If your real page count is small and your URL space is enormous, parameters are precisely the gap between those two numbers.
How to build a parameter signature from your logs
Raw log URLs are too granular to aggregate. Collapse them into signatures: the path template plus the sorted set of parameter keys, discarding values.
import re
from urllib.parse import urlsplit, parse_qsl
NUMERIC = re.compile(r"/\d+(?=/|$)")
def signature(url):
parts = urlsplit(url)
path = NUMERIC.sub("/{id}", parts.path)
keys = sorted({k for k, _ in parse_qsl(parts.query, keep_blank_values=True)})
return path, tuple(keys)
Run that over 30 days of verified Googlebot requests and aggregate three columns per signature: request count, distinct URLs seen, and distinct status codes. You now have a ranked list of the URL shapes Google is spending fetches on.
Two derived metrics do most of the diagnostic work:
- Requests per distinct URL. Low values (close to 1) mean Google is still in discovery mode on that shape — it's finding new combinations faster than it's revisiting old ones. That is the signature of an unbounded space.
- Value cardinality per key. Count distinct values per parameter key within a signature. A
sortkey with 4 values is bounded. Asessionidorrefkey with 40,000 values is a URL generator.
How to tell which parameters actually change the content
Logs tell you what was fetched, not what was returned. To classify parameters by content effect, sample them deliberately: for each signature, pull 20–50 URLs, fetch them with your own crawler, and hash the normalized main content (strip the nav, the footer, and any timestamp or CSRF token before hashing).
Then compute a fetch-to-unique-body ratio: distinct content hashes divided by URLs fetched. A ratio near 1.0 means every variant is genuinely different. A ratio of 0.05 means 95% of what Googlebot pulled down for that shape was content it already had.
| Parameter class | Changes main content? | Typical crawl cost | Appropriate lever |
|---|---|---|---|
Pagination (page) | Yes, different items | Proportional, legitimate | Link it, let it crawl |
Filter / facet (color, size) | Subset of a set | Combinatorial | Don't link deep combinations; canonical |
Sort order (sort, order) | Same items, reordered | Multiplies every other shape | Don't link; canonical to default |
Tracking (utm_*, ref, gclid) | No | Unbounded value cardinality | Canonical; avoid internal links carrying them |
Session / state (sid, cart) | No | Unbounded, one URL per visitor | Fix at the application layer |
View toggles (view=grid) | Presentation only | Doubles the space | Cookie or fragment, not a query key |
Which lever to pull, and what each one actually costs
These controls act at different stages, and choosing wrong is how sites lose pages they wanted.
- Stop generating the links. The only lever that prevents discovery. If a combination is never linked, never in a sitemap, and never in an embedded JSON payload Google can parse, it mostly doesn't enter the frontier. This is the highest-leverage and least-used fix.
robots.txtdisallow. Prevents the fetch, so it genuinely saves crawl. But Google can still index a disallowed URL from external links with no content of its own, and it cannot see a canonical tag on a URL it never fetches. Use it for shapes with no inbound links and no indexing value — session IDs, internal search results, infinite calendars.rel=canonical. Requires the fetch, so it saves no crawl in the short term. It consolidates signals and is a hint, not a directive. Over time, consistent canonicals plus consistent internal linking reduce recrawl frequency of the duplicates — but the reasonable inference, not documented fact, is that this decay is slow and partial.noindex. Also requires the fetch. Removes the URL from the index but keeps it crawlable; Google has said long-term noindexed pages tend to be crawled less often and their links eventually treated as nofollowed.
A common mistake is stacking disallow and noindex on the same URL. The disallow wins at the network layer, Google never reads the noindex, and the URL can persist as a bare listing.
What good looks like after you measure it
Re-run the signature aggregation monthly and watch two ratios. First, the share of verified Googlebot requests going to signatures with a fetch-to-unique-body ratio under 0.2 — that's your waste rate. Second, the share of requests going to URLs that have produced zero impressions in Search Console over the same window. Neither needs to hit zero; on a large faceted site, 10–20% waste is unremarkable, and 60% is a structural problem.
The payoff isn't a ranking lift you can promise. It's that fetches redirected away from duplicate variants land on pages that change — new inventory, updated prices, fresh editorial — which shortens the lag between publishing and recrawl.
What to do this week
- Pull 30 days of verified Googlebot log lines and aggregate them by path template plus sorted parameter key set.
- Rank signatures by request count, then compute distinct values per key to spot unbounded generators.
- Sample 20–50 URLs per top signature with your own crawler and compute the fetch-to-unique-body ratio on normalized content.
- For every shape with a low ratio, find the template that emits the links. Removing the link is a code change, not an SEO directive, and it's the one that actually reduces discovery.
- Reserve
robots.txtfor shapes with no external links and no indexing value; use canonicals where consolidation matters more than crawl savings. - Record the waste rate as a baseline so the next deploy can be evaluated against a number instead of an opinion.