GA4 Purchase Event Firing Twice
Nine times out of ten it is a reloadable order-confirmation page with no transaction-level guard. Here is how to confirm that, and the four other causes worth ruling out.
Get My Free Audit →Are you seeing this?
- GA4 reports more transactions than your store has orders for the same date range
- Revenue in GA4 is inflated, often by a suspiciously round multiple
- The same transaction_id appears more than once in your BigQuery export
- Purchase counts spike on days with no corresponding order spike
- Google Ads conversions imported from GA4 are higher than Ads' own conversion tracking
If two or more of these are true, you have duplicate purchase events.
Why it happens
The order-confirmation page is reloadable and nothing guards it.
WooCommerce's order-received endpoint renders every time it is requested. Refresh, back button, or a bookmarked confirmation URL each fire the purchase event again. This is the cause in the large majority of cases we audit.
GA4 is installed twice.
A gtag.js snippet in the theme header and a GA4 configuration tag in GTM, both live, both firing. Common after a migration where the old implementation was never removed.
Server-side and client-side both sending, with no deduplication.
A GTM server container was added alongside the existing browser-side tag and both send purchase with different event parameters, so GA4 has no basis to merge them.
The tag has two triggers.
A GA4 event tag firing on both a custom purchase trigger and an All Pages trigger, usually a leftover from debugging.
The dataLayer push is inside a loop or a re-rendered template partial.
Rare, but it happens on heavily customised checkouts, and it produces exact duplicates within a single pageview.
How to diagnose it
Step 01 — Quantify the gap.
Compare GA4 transactions against your store's order count for the same date range. A ratio near 2.0 points at a duplicate implementation; a ratio between 1.05 and 1.3 points at reloads, since only some customers refresh.
Step 02 — Watch a real order-received page.
Open GTM Preview or Tag Assistant and complete a test order. Count the purchase events on the confirmation page. More than one means causes 02, 04, or 05.
Step 03 — Refresh it.
With Preview still attached, reload the confirmation page. A second purchase event on reload confirms cause 01.
Step 04 — Check for a second implementation.
View source and search for gtag( and googletagmanager.com/gtm.js. Both present means GA4 may be running twice.
Step 05 — Check for duplicate transaction IDs.
If you have BigQuery export, group purchase events by transaction_id and count. Anything above 1 is a confirmed duplicate.
How to fix it
For cause 01 — the common one — mark the order as tracked and refuse to fire again:
Three things this does that the usual snippet does not: it stores the flag in order meta rather than a session or cookie, so it survives across devices and browsers; it records a timestamp rather than a boolean, so you can audit when tracking fired; and it clears ecommerce correctly by pushing a complete object rather than appending to a stale one.
For the other causes: remove the duplicate implementation (02), audit and remove the extra trigger (04), or move the push out of the repeated template partial (05). For cause 03, deduplication between client and server needs a shared event identifier — that is a larger piece of work and is covered on our server-side tracking page.
add_action( 'woocommerce_thankyou', 'propulse_ga4_purchase_once', 5, 1 );
function propulse_ga4_purchase_once( $order_id ) {
if ( ! $order_id ) {
return;
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
// Already pushed for this order — do nothing.
if ( $order->get_meta( '_propulse_ga4_tracked' ) ) {
return;
}
$items = array();
foreach ( $order->get_items() as $item ) {
$product = $item->get_product();
$items[] = array(
'item_id' => $product ? $product->get_sku() : '',
'item_name' => $item->get_name(),
'price' => (float) $order->get_item_total( $item, false, true ),
'quantity' => (int) $item->get_quantity(),
);
}
$payload = array(
'event' => 'purchase',
'ecommerce' => array(
'transaction_id' => $order->get_order_number(),
'value' => (float) $order->get_total(),
'tax' => (float) $order->get_total_tax(),
'shipping' => (float) $order->get_shipping_total(),
'currency' => $order->get_currency(),
'items' => $items,
),
);
echo '<script>window.dataLayer = window.dataLayer || [];';
echo 'window.dataLayer.push(' . wp_json_encode( $payload ) . ');</script>';
$order->update_meta_data( '_propulse_ga4_tracked', current_time( 'mysql' ) );
$order->save();
}How to verify the fix worked
- Place a test order and confirm exactly one purchase event in Preview
- Refresh the confirmation page and confirm no second event
- Check the order in wp-admin — the _propulse_ga4_tracked meta should be present with a timestamp
- After 48 hours, compare GA4 transactions against order count again; the ratio should sit near 1.0
- If you had inflated historical data, note the fix date — GA4 cannot retroactively correct it, and your year-on-year comparisons will have a discontinuity you should document
When this needs more than a snippet
If your ratio stays above 1.0 after the guard is in place, the duplication is upstream of the thank-you page — usually a second GA4 implementation or a client/server deduplication gap. Both need the full event map traced rather than a patch.
That is what our GA4 audit covers: reconciling every event against your order table until the numbers agree, market by market.
Tell us about your setup. We respond within 24 hours.
Common
Questions
Not reliably. GA4 applies some deduplication on transaction_id within a narrow window, but it is not a guarantee and it does not cover events arriving from different sources or across sessions. Treat it as a safety net that has already failed if you are seeing duplicates, not as the mechanism you depend on.
No. GA4 does not support deleting or correcting individual events retroactively. You can filter or annotate in reporting, and if you use BigQuery export you can deduplicate in your own queries — but the GA4 interface will always show the original numbers. Note the fix date so future comparisons account for it.
Because only some customers reload the confirmation page. A ratio near 2.0 means a systematic double-fire on every order; a ratio between 1.05 and 1.3 is the signature of reload-driven duplicates, which affect a subset of orders. The fix is the same.
Ready to Build Something That Works?
Book a free 30-minute strategy audit. No pitch deck, no pressure — just an honest look at your setup and what to fix first.
