Query Parameters That Multiply Your URL Count

Any change to a query string produces a different URL. ?ref=twitter, ?sort=price, ?sessionid=abc123 and ?utm_source=newsletter appended to one page are four addresses serving one piece of content, and parameter order counts too — ?a=1&b=2 and ?b=2&a=1 are distinct strings even though most applications treat them identically.

The consequence is combinatorial. A page with three independent optional parameters has eight reachable addresses before you count parameter ordering.

Sort parameters by what they do

The right treatment depends entirely on the parameter’s function, and the mistake is applying one policy to the whole query string.

Tracking parametersutm_*, ref, fbclid, gclid, affiliate codes. They do not change the content at all. The page is identical; the parameter exists so that something downstream can attribute the visit.

Filtering and sorting parameters?sort=price, ?colour=blue, ?page=2. They change what is displayed. Sometimes meaningfully, sometimes trivially.

Session and state parameters?sessionid=, ?cart=, ?csrf=. They are per-user and should never have been in a URL.

Content-defining parameters?id=4471 on a legacy CMS, ?product=widget. The parameter is the address; without it there is no page.

Four categories, four different answers.

Tracking parameters: canonical, never redirect

A tracking parameter must keep working, because someone clicked a link containing it and the whole point is that analytics reads it. So a redirect is wrong: stripping the parameter server-side removes it before it can be recorded.

The correct treatment is a self-referencing canonical that names the clean URL:

<!-- served at /pricing/?utm_source=newsletter -->
<link rel="canonical" href="https://example.com/pricing/">

Emit the canonical from the route, not from the request URL. A template that builds the canonical by echoing the current URL will happily include the parameters, which defeats the purpose and is a surprisingly common bug — grep for canonical tags containing ? in a crawl export and you will usually find some.

Remember that a canonical is a hint. It works well here because these variants are genuinely byte-identical to the clean URL, which is the case where the engine has least reason to disagree with you. The general limits are in canonical tags are a hint, not a command.

Session and state parameters: remove them

There is no configuration that makes session identifiers in URLs acceptable. Every share, every copy-paste, every referrer header leaks a session, and every one creates a unique URL that can be crawled and indexed.

Move state to cookies or headers. This is an application change, and it is the correct fix. Until it lands, mitigate with a Disallow on the parameter pattern and a canonical to the clean URL — but treat that as triage rather than a solution.

The historical version of this problem is worth recognising in inherited codebases: some older frameworks fell back to URL-based sessions when cookies were unavailable, silently, per-request. The symptom is a crawl report with thousands of URLs sharing a path and differing only in an opaque token.

Filtering and sorting: decide what deserves to be indexed

This is the category with real judgement in it, and the question to ask per parameter is: would someone search for this?

?sort=price-asc — nobody searches for a sort order. The sorted view is the same set of items in a different sequence. Canonical it to the unsorted view, and consider whether it needs to be crawlable at all.

?colour=blue on a product category — possibly. “Blue widgets” is a thing people search for, and if you have enough blue widgets to make a genuine page, that view may deserve to be indexed on its own terms. In which case it should not be a query parameter — it should be a path, /widgets/blue/, with its own title and description.

?page=2 — a distinct set of items, and it needs its own indexable URL rather than a canonical to page 1. Covered in pagination without rel=next and rel=prev.

The general rule that falls out: if a filtered view deserves search presence, give it a path; if it does not, keep it a parameter and keep it out of the index. Trying to make parameterised URLs into indexable landing pages produces the worst of both — thin near-duplicate pages at ugly addresses.

Where the parameter space is large rather than a handful of options, the problem changes character and becomes the one in faceted navigation and the crawl space it opens.

Content-defining parameters: leave them alone

?id=4471 as the sole identifier of a page is not elegant, but it is a working address and it may have years of inbound links. It needs a self-referencing canonical including the parameter, and nothing else.

If you are moving to clean paths, that is a URL migration: redirect the parameterised form to the new path, keep the redirect forever, and expect the old form to keep receiving traffic indefinitely.

Normalising what you can

Two cheap wins that reduce the surface without any indexing decisions:

Fix parameter order in the URLs you generate. If your templates emit parameters in a consistent order, you stop creating ordering variants yourself. External links will still arrive in arbitrary order, which the canonical handles.

Drop empty parameters. ?colour=&size= is a distinct URL from the clean one and represents no selection at all. Filter empty values out at generation time.

The robots.txt option and its cost

You can keep parameterised URLs out of the crawl with a pattern rule:

User-agent: *
Disallow: /*?sort=
Disallow: /*?sessionid=

This is the right tool when the URLs are generated, unlinked from outside, and expensive to serve. It is the wrong tool when the URLs already have external links, because a blocked URL cannot deliver the canonical that would have consolidated it — and it can remain indexed as an undescribed address. That trap is the subject of robots.txt cannot deindex a page.

Sequence it correctly: canonical first, confirm consolidation, then block if crawl volume is still a problem. Not the reverse.

Measuring the surface

Logs are the honest source, because they show what is actually being requested rather than what your templates emit.

# distinct parameter keys hitting the site, by request volume
awk '{print $7}' access.log | grep '?' \
  | sed 's/.*?//' | tr '&' '\n' | cut -d= -f1 \
  | sort | uniq -c | sort -rn | head -20

That list is your actual parameter inventory, and it routinely contains keys nobody on the team recognises — parameters from a retired campaign tool, from a plugin, from a partner’s link template. Each unknown key is a URL multiplier you were not aware of, and each one is worth tracing to whatever emits it.