CreativeCape

Why Your GA4 Purchase Numbers Don't Match Your Database

Duplicate purchases, refresh re-fires, consent blocking and currency drift — the five reasons GA4 revenue diverges from your orders table, and how to fix each one.

August 12, 2026·4 min read

Someone compares GA4 revenue to the orders table and the numbers do not match. It is one of the most common analytics support requests, and the cause is nearly always one of five things.

A note before the list: GA4 and your database will never match exactly. Consent, ad blockers and network failures guarantee a gap. The realistic target is GA4 reporting somewhat under your database, consistently. If GA4 is reporting more revenue than you actually took, that is not sampling noise — that is a bug.

Cause 1: refresh re-fires the purchase event

The most frequent cause of inflated revenue.

The purchase event fires on the confirmation page. The customer refreshes, or bookmarks it, or returns via browser history. The event fires again. One order, several purchases.

The fix is two layers:


const key = `purchase_${transactionId}`;

if (sessionStorage.getItem(key)) return;

sessionStorage.setItem(key, "1");

plus a stable transaction_id so GA4 deduplicates server-side. Use both — sessionStorage does not survive a new tab.

Symptom: GA4 revenue is higher than the database, and GA4 transactions exceed order count.

Cause 2: missing or unstable transaction_id

GA4's deduplication depends entirely on transaction_id. If it is absent, every purchase event counts. If it is generated with Date.now() or a random value, each re-fire produces a new id and dedup never engages.

It must be your order number, issued by the server. This also makes reconciliation possible at all — without a shared key you cannot compare row by row.

Symptom: duplicate purchases despite a dedup guard.

Cause 3: firing before server verification

If the event fires when the payment gateway's client-side handler reports success, you capture transactions that never completed: failed signature verification, gateway timeouts after the callback, cancelled captures.

Fire purchase only after your server has verified and persisted the order, using the order number the database issued.

Symptom: GA4 shows transactions with no matching order row.

Cause 4: consent blocking the tag

Now the leading cause of under-reporting.

With Consent Mode configured and analytics_storage denied, GA4 tags do not write cookies and, if you have set them to require consent explicitly, may not fire at all. Every user who declines is missing from revenue.

This is expected and correct — but it must be understood, or you will chase a "bug" that is your privacy implementation working.

Quantify it: measure your accept rate and check whether the shortfall is roughly proportional. If 30% decline and GA4 is 25–30% low, the system is behaving as designed.

Symptom: GA4 consistently and proportionally below the database.

Cause 5: currency and tax mismatch

Three variants:

  • Currency mismatch. You price in USD but charge through a gateway in INR, and send one figure with the other's currency code. GA4 converts using the property currency and the totals diverge.

  • Tax and shipping inconsistency. GA4's value should be the total the customer paid. If you sometimes send the pre-tax subtotal, revenue sits low by exactly the tax rate.

  • Discounts applied twice, or not at all. The value is computed client-side from list prices while the server charges a coupon-adjusted amount.

Send the server-computed total — the number you actually charged — never a client-side recalculation.

Symptom: a consistent percentage offset rather than a random one. A consistent offset is a formula error; a random one is a firing error.

A reconciliation checklist

Take one full day, at least 48 hours old so processing has completed:

  1. Export GA4 purchases with transaction_id and revenue (the BigQuery export is ideal).

  2. Export orders for the same window.

  3. Join on order number and compare.

Then classify:

  • In GA4, not in the database → firing before verification, or duplicates

  • In the database, not in GA4 → consent, ad blockers, or a tag failing to fire

  • In both, different values → currency, tax or discount handling

Each bucket points at exactly one of the causes above.

Monitoring for drift

Once reconciled, keep it reconciled. Implementations rot — a checkout refactor silently changes the payload and nobody notices for a quarter.

  • A weekly automated comparison of GA4 and database revenue, alerting past a threshold

  • An alert if GA4 transactions ever exceed order count, which is always a bug

  • Re-run the reconciliation after any checkout or payment change

Trust in analytics is slow to build and instant to lose. A number nobody believes is worse than no number, because it still gets used in decisions — just with an argument attached.


→ Get a tracking audit

Tagged with
#ga4#purchase event#revenue mismatch#transaction_id#data quality#ecommerce tracking

Found this useful? Share it.

Keep Reading

Related articles

Booking Q2 2026 Projects

Ready to Build Something Great?

From idea to launch — let our senior engineers build, ship and scale your next product. No commitment, just a conversation.

Senior Engineers
On-Time Delivery
Enterprise-Grade
Free Consultation

Free 30-min discovery call

Talk to a senior engineer — not a salesperson.

We'll review your goals, suggest the leanest path forward, and send a clear proposal within 24 hours.

24h

Response Time

100+

Projects Delivered

No commitment · No automated bots · Fully transparent