Field Fix

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 →
Symptoms

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.

Causes

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.

Diagnose

How to diagnose it

01

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.

02

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.

03

Step 03 — Refresh it.

With Preview still attached, reload the confirmation page. A second purchase event on reload confirms cause 01.

04

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.

05

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.

Fix

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.

PHP
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();
}
Verify

How to verify the fix worked

  1. Place a test order and confirm exactly one purchase event in Preview
  2. Refresh the confirmation page and confirm no second event
  3. Check the order in wp-admin — the _propulse_ga4_tracked meta should be present with a timestamp
  4. After 48 hours, compare GA4 transactions against order count again; the ratio should sit near 1.0
  5. 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
Escalation

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.

Get Your Free Audit

Tell us about your setup. We respond within 24 hours.

FAQ

Common
Questions

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.

Get My Free Audit →