Ten server-log rows do not prove ten paid Google Ads clicks. Neither does one GCLID prove exactly one click. A request, an identifier, a session and an ad-platform metric measure different things. Use your logs to reconstruct what reached the site; use Google Ads reporting and billing records to investigate what was counted and charged.

This guide gives you a ten-row example, a local reconciliation command and an IP review tool. Start with one landing page and one fixed time window. You can complete the counting exercise without changing a campaign.

For the cross-platform comparison, use the Google Ads clicks versus GA4 sessions diagnostic. Request rows and analytics event rows need different counting rules.

1. Keep four different counts

What each number can establish
MeasureWhat you countedWhat it cannot establish alone
Server requestsRows recorded by a particular log layerSeparate ad clicks, people or sessions
Distinct observed GCLIDsDifferent non-empty values found in the selected dataA one-to-one count of clicks or charges
Analytics sessionsVisits measured under that property's collection and session rulesEvery click that occurred before the page or tag loaded
Google Ads clicks and costThe platform's report for the chosen account, scope and datesThe full sequence of requests or form outcomes on your server

The important edge case is documented by Google: a GCLID can originate at impression time and be used again when the same user clicks that ad again. Multiple clicks can therefore sit in one GCLID row in the click-view report. Counting unique IDs is useful for organizing evidence, but it is not a billing reconciliation.

Google Ads: how auto-tagging works, checked November 3, 2026.

The opposite mismatch matters too. A person can reopen a bookmarked URL containing an old GCLID. That generates website traffic without a new ad click. A reload, a copied link and a second request through another logging layer can also preserve a value. Your access log does not record which of those explanations caused the request.

Google describes bookmarked GCLIDs, redirects and clicks-versus-sessions discrepancies. A bookmark visit is not a new charged ad click.

2. Ten requests, three distinct GCLIDs

Download the ten-row synthetic log CSV. Its addresses are documentation examples and its identifiers are invented. It contains no customer traffic and should never become an exclusion list.

The counts you can reproduce from the sample
RowsRecorded patternWhat the data establishes
1–4One address requests /quote four times with GCLID AFour requests and one observed value; the cause of the repeats is unknown
5–6The address requests /quote with GCLIDs B and CTwo additional observed values
7A second address requests /quote with GCLID AThe same value appears under two addresses; this does not identify one person
8The second address carries only WBRAIDA separate identifier-bearing row; no additional GCLID
9The second address requests /quote without a click IDAn untagged landing request, with no inferred acquisition source
10The first address requests /assets/app.jsAn asset request, outside the selected landing path

The result is 10 request rows, 9 requests to the selected landing path, 7 GCLID-bearing rows and 3 distinct GCLIDs across the file. The first address has 3 distinct GCLIDs; the second has 1. Adding those per-address counts gives 4 because A appears in both groups. Group totals are not always additive.

The missing answer is deliberate

The example establishes neither a session count nor a paid-click count. There is no analytics session record, Google Ads report or billing adjustment in this CSV. Do not fill those gaps with a deduplication rule.

3. Reproduce the count locally

Download the sample, open a terminal in its folder and paste this command. It reads the CSV on your computer and prints a summary. It makes no network request and changes no file. For your own export, keep the four column names and replace /quote with the exact landing path you are investigating.

python3 - repeat-click-log-example.csv <<'PY'
import csv, json, sys
from collections import Counter, defaultdict
from urllib.parse import parse_qsl, urlsplit

totals = Counter()
seen = set()
by_ip = defaultdict(set)
with open(sys.argv[1], newline="", encoding="utf-8-sig") as source:
    reader = csv.DictReader(source)
    required = {"client_ip", "recorded_at", "method", "path"}
    if not required <= set(reader.fieldnames or []):
        raise SystemExit("Export the four normalized columns first")
    for row in reader:
        totals["request_rows"] += 1
        url = urlsplit(row["path"])
        if row["method"] != "GET" or url.path != "/quote":
            continue
        totals["landing_requests"] += 1
        query = parse_qsl(url.query, keep_blank_values=True)
        ids = [v for k, v in query if k == "gclid" and v]
        if len(ids) > 1:
            totals["ambiguous_gclid_rows"] += 1
        elif ids:
            totals["gclid_rows"] += 1
            seen.add(ids[0])
            by_ip[row["client_ip"]].add(ids[0])
        elif any(k in ("gbraid", "wbraid") and v for k, v in query):
            totals["braid_only_rows"] += 1
        else:
            totals["landing_rows_without_id"] += 1
print(json.dumps({
    **totals,
    "distinct_observed_gclids": len(seen),
    "distinct_gclids_by_ip": {ip: len(ids) for ip, ids in sorted(by_ip.items())}
}, indent=2))
PY

The summary should show distinct_observed_gclids: 3, braid_only_rows: 1 and landing_rows_without_id: 1. Rows containing several non-empty GCLID parameters go into an ambiguity count instead of silently choosing one. Parameter names and identifier values retain their case; query decoding happens once.

Export only the chosen period before running the command; it filters the landing path but does not select dates for you. Use a normalized export rather than adapting a regular expression to an unfamiliar log format. Keep timestamp offsets and original evidence in your local source file. Before sharing a summary, remove customer IPs, click IDs, cookies, phone numbers and query values you do not need.

4. Check what your log actually recorded

  1. Select a single observation layer. An edge request and the matching origin request can represent one browser request at two layers. Preserve a request ID where available; do not sum both files and call the total visits.
  2. Verify the client-address field. Behind a CDN, the connection peer may be a proxy. Use the field supplied by your trusted proxy configuration. Do not accept an arbitrary client-supplied forwarded header as authoritative.
  3. Separate the request target from the referrer. A GCLID in a referrer on an image or script request does not establish a new ad arrival. The command above inspects the normalized request target only.
  4. Limit the cohort. Use consistent start/end timestamps, timezones, hostname, landing path and campaign scope. Keep redirects and errors visible in a separate diagnostic view.
  5. Check URL handling without clicking a live ad. Open a test URL with a clearly synthetic value and follow the redirects. Record whether the final landing URL preserves the parameter. A successful redirect check does not validate a real Google click ID.

For nginx, the real-IP module documentation defines which proxy addresses are trusted to supply a replacement client address. Check the equivalent configuration for your own stack.

Use the free landing URL checker to compare before-and-after URLs. If the tag cannot load, the request is blocked, or the parameter disappears, investigate that path before interpreting a click/session gap as fraud. For missing business outcomes, use the conversion-signal diagnostic.

For a repeated address that may be a privacy relay, check Apple’s range feed and the corresponding site outcomes before treating it as one source of abuse.

5. Match the response to the evidence

  • Repeated requests with the same value: inspect request IDs, path, timestamps and browser behavior. A repeated value alone does not tell you whether another ad click occurred.
  • Several values behind one address: examine the outcomes and network context. A shared office or mobile exit can serve multiple legitimate people.
  • A hosting-network match: treat it as context for review. It identifies the network, not the intent of the visitor. Do not exclude a whole range because its provider name looks unfamiliar.
  • Few site sessions but more Ads clicks: inspect page delivery, tag execution, consent configuration and redirect behavior. Do not weaken consent controls to force two totals to match.
  • Suspicious billed activity: retain the date range, campaigns, relevant IDs, server evidence and business outcomes for an investigation. Review the platform's invalid-click reporting and billing adjustments separately.

Google's invalid-traffic guide explains shared IPs, filtered activity and the evidence for investigation. A suspicious visit reaching your site does not establish a new charge. See the invalid-click investigation guide for the reporting route and the same-IP guide for repeat-visitor context.

Google Ads: invalid traffic and investigation evidence.

6. Review addresses, then decide what to exclude

Paste your selected log data into the builder below to organize distinct observed GCLIDs by address and review known cloud-network matches. Its default log filter requires 3 distinct GCLIDs per address. Lower the filter to inspect smaller groups. BRAID-only rows stay separate. A threshold is a review rule, not a fraud finding.

The raw-log builder reads the first public address in each line and can find identifiers anywhere in that line. Normalize your client-address field and request target first when the raw format includes proxy addresses or referrers. Keep range combining off during the initial review. The synthetic addresses above are for the exercise only.

Free tool

Build your IP exclusion list

Paste addresses from a log, a CSV export or a report, in any layout. You get a clean list in the format Google Ads accepts, with repeats removed and the most active addresses first. Paste a server log to count distinct GCLIDs from each address.

addresses share the first three parts

0 entries

Runs in your browser. Nothing you paste is sent anywhere.

Need a different investigation? Use the placement exclusion builder for inventory review or the conversion-action audit for bidding-signal configuration. A log counter cannot diagnose either of those from click IDs alone.

Move from a count to a visitor investigation

AdProtektor adds visitor histories, risk explanations and configurable protection to the site-side investigation. Review what a flagged visitor did, choose rules that fit your buying cycle and check whether a supported protection action reached its destination. A recorded decision is not proof of a refund or a prevented charge.

The public documentation explains how detection works and automatic blocking and its delivery requirements.

Start a free trial and inspect your paid traffic, or follow the traffic-audit checklist first. Tracking installation and actual visits are needed to build evidence on your site. For conversion delivery rather than traffic classification, see ConversionOS.

To separate browser capability from visitor identity, run the local JavaScript and browser-signal lab. A shared header, a received page report and an automation flag answer different questions.

Before preparing exclusions from a CDN or load-balancer log, use the client-IP and trusted-proxy diagnostic to separate the transport peer, selected forwarded address and unresolved observations.

If Ads reports clicks but you cannot find a landing request, use the missing-arrival diagnostic to check click types, destinations, CDN/cache behavior and log coverage before interpreting the gap.

To distinguish a client-supplied label from an optional authorized account match, use the GCLID evidence lab and lookup guide. Neither a parameter nor an account match certifies the present enquiry.