Dilshat Rakhimov
July 2026 · 10 min read

Here's a pattern that shows up on almost every lead-gen account. Google Ads is full of leads - applications are landing, the CPL looks healthy - but the thing that actually pays the bills never shows up in the platform. In lending that's the loan disbursement; in a subscription it's the first payment; in e-commerce it's the fulfilled order. When leads keep flowing but the revenue column stays at zero, the cause is almost always the same: somewhere between the click, the redirect tracker, the landing page, and the server-side conversion, the gclid chain is broken.

Let's trace it on a lending PWA aggregator. Someone clicks a Google ad, fills in an application, and the disbursement - the actual money - gets recorded later, when the lender posts back to the backend with no browser in the loop. Sitting between the ad click and the landing page is a redirect tracker, the Keitaro or Binom kind. That hop is exactly where the gclid was dying.

gclid chain cover

What the gclid chain is, and where it snaps

gclid (Google Click Identifier) is the token Google Ads tacks onto an ad's final URL when auto-tagging is on. Google's whole attribution model hangs off it. No gclid in the session and Google Ads has no idea the visit came from one of its clicks, so a server-side conversion that fires two days later has nothing to tie it back to the campaign.

Now drop a redirect tracker into the path. The ad's final URL doesn't point at the landing page; it points at the tracker: https://track.example/<token>. The tracker fires a 302 and sends the user on to the landing page: https://app.example/?clickid=<subid>. Here's the trap - on that redirect the tracker carries its own clickid forward but leaves the gclid behind:

Google Ads (auto-tagging)
→ track.example/<token>?gclid=ABC… ← gclid is still here
→ 302 →
→ app.example/?clickid=xyz ← gclid is gone

The landing page loads with no gclid. Everything downstream still looks healthy: the application gets created, the tracker's clickid rides through to the backend, leads pile up. But GA4 never tags the session as Google / paid, because there was no gclid in the URL to read. So when the disbursement lands - a server-side purchase event - there's nothing to connect it to the Google click. You get the leads (the tracker's clickid holds those together) but not the disbursements (those need the gclid, and it died on the 302).

The second bug, hiding in plain sight: gclid written, never read

Even on the clicks where the gclid did make it through and got captured at submit, a second problem turned up - this one in the backend, not the tracker. The application-save path wrote gclid into the database just fine. But the function that later pulled the application back out to fire the conversion never read that column. Textbook silent gap: the field exists, the value is sitting in the row, and at send time nobody plugs it into the purchase. From the outside it looked identical to the first bug - "disbursements aren't attributed" - even though the data was right there in the database. You don't catch this in a dashboard. You catch it by following one value from capture to send, field by field.

Reconnecting the chain

There's no single switch that fixes this. You rebuild the gclid's whole path, from the click to the money.

1. Carry gclid through the redirect tracker. Add gclid to the tracker's offer/flow URL so it survives the 302 next to clickid:

app.example/?clickid={subid}&gclid={gclid}

{gclid} is a valid Google Ads source macro inside the tracker. Run a test click afterward and both parameters should land on the page. One compliance note worth knowing: parallel tracking only constrains the tracking template, not a Final URL redirect. So pointing the Final URL at the tracker (on a subdomain of the same root domain as the landing page) is fine - it doesn't break anything.

2. Grab gclid and client_id on the landing page and store them with the application. Pick up _ga (the GA4 client ID) at the same time - you'll need it to make the server-side purchase land in the right session.

3. Fire the purchase server-side when the money lands. On disbursement, send GA4 a purchase over the Measurement Protocol: value (your commission), transaction_id = pb_<id>, client_id from the _ga cookie, plus gclid and hashed email/phone for Enhanced Conversions. The disbursement comes in as a postback with no browser attached, so front-end GTM will never see it - it has to be a server-side hit.

4. Pull purchase into Google Ads. Make purchase a Key event in GA4 and import it into Google Ads (GA4 → Ads import). Now the campaigns optimize toward disbursements instead of cheap leads. One rule: don't count it twice - use either the GA4 → Ads import or a direct Ads API upload, never both, or every conversion doubles.

Dedup across the whole chain rides on transaction_id = pb_<id>.

What actually bit on the way to production

The plan above is clean. The server wasn't. A few forced workarounds are worth knowing before you hit them yourself.

We had to read _ga on the server, not the front end. The box only had the compiled dist - no front-end source - so rebuilding it to add a client_id capture wasn't an option. Instead, the _ga cookie rides along on the same-origin application POST, and the backend reads client_id straight off req.headers.cookie. No front-end rebuild required.

The Measurement Protocol hit goes straight to Google, not through sGTM. The instinct is to route the server hit through your own server-side GTM. Don't bother: a stock sGTM container with a GA4 client returns 400 on /mp/collect - the GA4 client only listens on /g/collect, not the MP endpoint. And for a pure server-to-server call there's no first-party upside to sGTM anyway - no browser, nothing to block. So the purchase goes directly to https://www.google-analytics.com/mp/collect, and a 204 means it took.

Cloudflare's Bot Fight Mode eats server-to-server POSTs. If the domain sits behind Cloudflare, Bot Fight Mode can quietly drop server POSTs to /mp/collect. Add a WAF skip for that path, or send the MP hit around the proxy.

Capturing client_id isn't 100%, and that's fine. On live submits, _ga comes through about 90-92% of the time; the misses are ad blockers stripping the GA cookie. That's the expected ceiling, not a bug to chase.

The takeaway

"Lots of leads, no revenue" in Google Ads is hardly ever the algorithm's fault. It's almost always a click-id chain that snapped somewhere. Follow the gclid from the ad, through every redirect, onto the landing page, and out to the server-side money event - literally trace it by eye, field by field, with a test click. The break is usually in one spot: the redirect tracker's 302, a column the backend writes but never reads, or a server-side purchase that never gets sent when the money actually lands.

And the bigger point: attribution isn't done when the click gets tagged. It's done when the money event carries the click ID back. Until the purchase carries the gclid (and its cousins in the other channels), Google will optimize for the cheapest lead it can find instead of a paying customer - and it'll do exactly that, exactly as well as you asked it to.

FAQ

Google Ads shows leads but no revenue. What is the usual cause?

A broken gclid chain. The most common break is a redirect tracker sitting in the middle of the path that drops the click ID before anything stores it.

The gclid is in my database — does that mean the chain works?

Not necessarily. The second bug in this case was a gclid that was written and never read: the column was populated, and nothing downstream picked it up when the conversion was sent.

How do you reconnect the chain?

Send the conversion from the server with the stored gclid attached — GA4 Measurement Protocol on the analytics side, Enhanced Conversions on the Google Ads side. And note that a 200 from the API is not proof the conversion was attributed to the click; check the import diagnostics.