Google Ads conversion deduplication: order IDs, webhook retries, and inflated conversion counts
Duplicate conversions are the quietest way to lose money in Google Ads. Nothing errors. Nothing looks broken. Your dashboards simply report more conversions than occurred, your CPA looks better than it is, and Smart Bidding cheerfully spends against economics that don't exist. By the time finance notices that ad-platform revenue doesn't reconcile with the bank, the account has been optimising toward phantoms for months.
Where duplicates come from
Thank-you page reloads. The classic. A conversion tag on the confirmation page fires again on refresh, on back-button revisits, and on the bookmark the customer saved to check their order. Without a transaction ID, each fire is a fresh conversion.
Webhook retries. The server-side equivalent. Payment providers deliver webhooks at-least-once by design: if your endpoint responds slowly or the provider is uncertain, it sends the event again — sometimes minutes later, sometimes days, during incident recovery. An integration that naively forwards each delivery to Google Ads converts reliability engineering into fake revenue.
Parallel pipelines. A browser tag and a server-side stream both reporting purchases — common mid-migration to sGTM, and occasionally left that way permanently because "the numbers looked better".
Event taxonomy overlap. "Purchase" and "Subscription started" both marked Primary, both firing on the same checkout. Not a duplicate technically; a double-count practically.
The mechanics of getting it right
Google Ads deduplicates browser conversions natively when you pass a transaction_id: identical IDs within the conversion window count once. So rule one is simply: every purchase event, from every source, carries the order ID from your commerce system. Not a timestamp, not a session ID — the durable identifier your database uses.
Webhook-side, deduplication has to happen before Google ever sees the event, because retries can arrive outside any platform window and offline conversion imports don't always dedupe the way you hope. The pattern we deploy: the webhook consumer writes each event's provider ID and order ID to a small idempotency store; an event whose ID has been seen is acknowledged and dropped. Only first-seen events proceed to the sGTM container and onward. It is perhaps a day of engineering, and it makes the whole downstream stack trustworthy.
For subscriptions there is a second decision: what counts as a conversion at all? A rebill is real revenue but not a new acquisition — sending renewals as purchase conversions inflates the acquisition signal with customers you already own. We separate trial start, first payment, and renewal into distinct actions, with only the first payment feeding acquisition bidding. Renewals belong in value-based reporting, not in tCPA math.
How to check your own account today
Three checks, an hour total. Export thirty days of conversions segmented by conversion action and compare purchase-action counts against your payment provider's transaction count for the same window — a ratio persistently above 1.0 is your answer. Second, check whether your purchase tags pass a transaction ID at all; a surprising share of accounts never wired it. Third, list your Primary conversion actions and ask whether any pair can fire on the same user action.
If you find inflation, fix the pipeline before correcting the history, and expect the uncomfortable part: your true CPA is higher than reported, and bid targets calibrated to inflated counts will need re-basing. That transition is manageable when planned — we covered the failure mode in the measurement collapse article — and brutal when discovered by accident.
Deduplication is unglamorous plumbing. It is also the difference between an account optimised toward reality and one optimised toward an accounting error.