The fingerprint is a hash of the header tuple a client sends, and it deliberately excludes the IP address. That is the entire point.
user-agent + accept + accept-language + accept-encoding
+ sec-ch-ua + sec-ch-ua-platform
+ sec-fetch-site + sec-fetch-mode + sec-fetch-dest + sec-fetch-user
+ protocolA rotating-proxy fleet changes its exit address on every request and changes nothing else, because the whole value of the fleet is that it is one automation stack wearing many addresses. Hashing everything except the address is what collapses the fleet back into a single row.
Figure 1 — the fleet’s own evasion becomes its signature. The distinct-address count is computed over a twenty-four hour window centred on the session, and five or more on non-mobile networks is the threshold.
It is computed in as few Solr queries as possible, not one per fingerprint: sessions are bucketed by the hour they ended, every fingerprint in a bucket shares one window, and the whole bucket is answered by a single facet with a nested distinct count, chunked to stay well under the term-filter cap. A batch of four hundred fingerprints is a handful of queries, not four hundred.
Carrier-grade NAT genuinely puts thousands of real people behind a handful of addresses, so the rule excludes mobile networks. A verified search crawler is excluded too, because it legitimately crawls from a large address pool with one fingerprint. Both exclusions mean an ASN misclassification turns into a batch of false positives — if a whole organisation is showing up as a fleet, check the network type for their ASN first.
The tuple has eleven components. With plain combined, nine of them are not logged — only the User-Agent and the protocol version survive — so the fingerprint degenerates to roughly a hash of those two. That still catches a fleet using one User-Agent across many addresses. It does not catch a fleet that also rotates its browser version: that fleet splits into one cluster per version, and each cluster can fall below the threshold.
This is measured rather than theorised. A real capture committed to the Loghound repository holds 75 requests from 13 addresses, every one in a different network, each fetching exactly one page and then leaving:
| Fingerprint (User-Agent family) | Distinct addresses | Fleet rule fires? |
|---|---|---|
| Windows, Chrome 147 | 5 | yes |
| Linux, Chrome 150 | 3 | no |
| Windows, Chrome 150 | 2 | no |
| Windows, Chrome 151 | 1 | no |
| Linux, Chrome 152 | 1 | no |
| Linux, Chrome 148 | 1 | no |
human
Not “unknown” — human, the same verdict a real reader gets, because the only rule that fires on them is the fifteen-point single-page one. That is the honest outcome, it is the worst case in this whole documentation, and it is exactly the argument for logging more headers. They are caught immediately once the beacon reports.
Three timestamps from that capture make the point better than any explanation: three different addresses on three different networks began a session in the same second with an identical fingerprint, and two more did it again seven minutes later. Five independent humans do not do that.
Do not normalise the browser version out of the User-Agent before hashing. It would merge the fleet’s clusters — and it would also merge every Windows Chrome visitor on your site into one cluster of thousands of addresses. On plain combined that is not a tuning decision, it is a switch that labels your entire audience a proxy fleet.
What to do instead is log the recommended format. One header alone carries enough entropy across real humans to keep the fingerprint discriminating, while the fleet — which sends one header set — collapses into one cluster.
It comes from Solr’s distinct-value function, which is exact below roughly a hundred and approximate above it. That matters for a number the product presents as evidence, so the panel says so in the fingerprint view’s caption, in the networks hint and in the overview visitor caption.
The view that shows all this is Fingerprint clusters, and the rule it feeds is on the scoring page. The cluster spans a whole shared index pair deliberately — see joining a pair.