How to detect duplicate transactions in GA4

Duplicate purchase events are the quietest revenue inflation in GA4: everything looks plausible, ROAS looks a little better than reality, and nobody notices until a finance reconciliation fails. Here is how we detect them in BigQuery, what causes them, and the one revenue field that inflates numbers without any duplicate at all.

The short answer

Query your GA4 BigQuery export for purchase events grouped by transaction_id: any ID with more than one event is a duplicate. Then check two subtler inflators: the items array’s item_revenue field, which can carry the full cart total on every item row, and your reconciliation logic against the backend, where comparing against the wrong order status or the wrong order ID makes real duplicates invisible.

The five-minute detection query

On the events_ tables, filter event_name = ‘purchase’, UNNEST nothing yet, group by ecommerce.transaction_id and count. Anything above one is duplication, and the revenue delta is your inflation. Two practical notes from production: always convert event_timestamp with TIMESTAMP_MICROS and your local timezone before comparing days against backend reports, and run the check across at least a 30-day window, because some duplicate patterns (browser session restore, email revisits to the thank-you page) are rare per day but material per month.

Where duplicates come from

The usual suspects, in the order we find them: the thank-you page firing purchase on every load, so refreshes, back-button returns and bookmarked confirmations each add a transaction. Single-page-application storefronts re-triggering the purchase event on route changes or component re-renders, a pattern we see repeatedly on SPA frameworks. GTM setups with two triggers reaching the same tag after a migration. And payment flows that return the customer to the confirmation step twice. GA4 deduplicates identical transaction_ids within a session in its own reports but your BigQuery export keeps every event, which is exactly why the export is where you measure the truth.

The item_revenue trap: inflation without duplicates

One field deserves its own warning. In the items array, item_revenue is meant to carry per-item revenue, but real-world implementations frequently write the full cart total into every item row. Sum item_revenue across an UNNESTed items join and a three-item order triples its value: no duplicate events anywhere, revenue inflated all the same. Our rule in production: purchase revenue comes from ecommerce.purchase_revenue at the event level, and item-level analysis validates itself against it before anyone trusts a category breakdown.

Reconciling against backend truth

Detection only matters against a reference. Two mistakes break reconciliations before they start. First, order status: the correct backend filter is usually “non-cancelled”, not “completed”, because completed excludes orders that are real revenue in analytics terms but still mid-fulfillment. Second, order ID mapping: commerce platforms often carry two identifiers, the storefront order ID and the ERP order ID, and GA4’s transaction_id matches only one of them. Confirm which one with a sample join before declaring a mismatch. A reconciliation that matches on the right ID with the right status filter typically explains most of the GA4-vs-backend gap; what remains is your genuine duplicate and tracking-loss story.

What it costs you if ignored

Duplicate inflation flows straight into ROAS: revenue is overstated on exactly the sessions that convert, so paid channels look better than they are, and the error compounds in LTV models built on the same events. The framing matters too: an inflated number is not a growth story, it is a measurement problem, and treating measurement problems as performance signals is how budgets drift to the wrong places.

The systematic version

Duplicate detection is one of the daily automated checks in our Measurement Health Framework: the same query logic, running on our clients’ exports every morning, flagging any day where a transaction_id count exceeds one. A one-off cleanup fixes today; the daily check is what keeps the thank-you page honest after the next release.

FAQ

Does GA4 deduplicate transactions by itself?

Should we fix duplicates in tracking or filter them in queries?

Our GA4 revenue is lower than the backend, not higher. Same problem?

What is a normal duplicate rate?

Not sure if your data is telling the truth?