Conversions started counting twice the week server-side sending was switched on
In short
Reported conversions jump on the day server-side sending goes live while order volume stays flat: deduplication is failing at one of 5 points — identifiers generated independently, a server copy arriving outside the dedup window, mismatched event names, a missing key field, or a retry re-sending with a fresh identifier. A paired-event report over one day says which.
Key takeaways
- Confirm the step change against back-end order rows first. A 40% jump with flat orders is duplication; a jump with rising orders is not.
- The diagnostic artefact is one row per order carrying both event identifiers and the arrival gap between transports — nothing else identifies the cause.
- Independent identifier generation is the most common failure and the easiest to prove: the 2 columns never match on any row.
- A retry loop with a fresh identifier per attempt produces duplicates that arrive minutes apart and inflates the tagging-server bill at the same time.
- Suppressing one transport stops the bleeding but forfeits the coverage you moved server-side to get, so treat it as a time-boxed measure.
The symptom is specific enough to be diagnosable: reported conversions step up on the rollout date, the shape of the curve is otherwise unchanged, and the order table underneath is flat. That is deduplication failing, and it fails in one of 5 places. Either the browser and the server generate their identifiers independently, or the server copy arrives after the platform's dedup window has closed, or the 2 copies carry different event names, or a field the platform keys on is missing from one side, or a retry is re-sending the same conversion with a new identifier each time.
All 5 present identically in a dashboard, which is why guessing is expensive. One report — a day of conversions with both identifiers side by side — separates them in about an hour. What an event identifier is and where it should come from is a separate subject, covered in the value that decides one conversion or two; this page assumes you have one and it is not working.
Confirm it is duplication, not growth
- Plot reported conversions and back-end order count on the same daily axis for the 14 days either side of the rollout. Duplication shows as a step in one series only, on a single date.
- Measure the ratio. A clean double is close to 2.0; anything between 1.2 and 1.9 means duplication is partial, which is itself diagnostic — the browser copy is being lost for some share of traffic, often the consent-denied share.
- Check the value column, not just the count. If reported revenue doubled while orders were flat, the duplicate copies carry a value and every downstream bid signal is being fed twice.
- Confirm the rollout date from the deployment record rather than memory. A step that predates the server rollout by 3 days is a different problem with the same shape.
The paired-event report
Take one completed day. For every order created that day, assemble a single row from your own logs — not from the platform, which will not show you what it discarded. Five columns are enough.
| Column | Source | What a healthy row looks like |
|---|---|---|
| order_id | Commerce back end | Present on every row, unique |
| browser_event_id | The tag's outbound request, logged client-side or at your endpoint | Populated, and identical to the server value |
| server_event_id | The conversion request your server sent | Populated, and identical to the browser value |
| event_name, both sides | Both outbound payloads | Byte-identical strings, including case |
| arrival gap | Server send timestamp minus browser send timestamp | Seconds, not hours. Anything beyond minutes is a candidate cause |
Sort by the gap column and read the extremes. Rows where the identifiers differ on every record point at generation. Rows where they match but the gap is measured in hours point at the window. Rows with several server sends for one order and a different identifier on each point at retries. This is one query against your own request logs, and having those logs is one of the practical arguments for a first-party endpoint you control.
The five failure points, ranked
| Failure point | Signature in the paired report | Fix |
|---|---|---|
| Independent generation | The 2 identifier columns never match on any row; both look well-formed | Generate once, from the order, and pass the same value to both transports |
| Arrival outside the dedup window | Identifiers match, arrival gap measured in hours or days | Send the server copy in the request path or from a queue with a latency budget, not a nightly batch |
| Event name mismatch | Identifiers match, names differ by case, prefix or a legacy alias | Pin both names to one constant in the data layer specification and assert it in a test |
| Missing key field | Identifiers match, gap small, platform still counts twice | Read the platform's current dedup rules and send every field it keys on, on both transports |
| Retry with a fresh identifier | Multiple server rows per order, different identifier on each, minutes apart | Make the identifier a function of the order, and make the send idempotent |
Independent generation is first because it is the default outcome of a rollout done by 2 people. The tag generates a random value at fire time; the server generates its own at send time; both are valid, neither is shared. It is also the cheapest to prove — a single query showing zero matching pairs — and the one fix that makes the other 4 easier to reason about.
The two that look alike from outside: window and retries
Both produce duplicates with matching-looking identifiers, and both are usually introduced by well-intentioned reliability work. The distinguishing evidence is in the count of server sends per order. One send arriving late is a window problem. Several sends, each with a new identifier, is a retry problem wearing the same clothes.
The window case comes from batching. Somebody moves conversion sending into a nightly job because it is tidier, and every conversion now arrives many hours after its browser twin. Platforms publish a deduplication window and it is not generous; treat anything beyond a few minutes as unsafe and read the current value from the platform's own documentation rather than assuming. If your architecture genuinely requires batch, accept that you are choosing one transport rather than 2.
The retry case is worse because it scales. A queue consumer that regenerates the identifier on each attempt turns one transient network error into 3 or 4 counted conversions, and the same loop shows up in your infrastructure bill — the mechanism behind the tagging server bill climbing faster than traffic. Derive the identifier deterministically from the order and the retry becomes harmless.
A retry that changes the identifier is not a retry. It is a new conversion, sent by a system that has forgotten what it was doing.
When the identifiers match and it still counts twice
This is the case that sends teams round in circles, and the cause is almost always that deduplication is not keyed on the identifier alone. Platforms combine it with other attributes — the event name, sometimes the action source, sometimes a user-level field — and a copy missing one of those is treated as a different event. The rules differ per platform and they change, so read the current documentation for the platform you are sending to rather than porting an assumption from another one.
- Send the same event name on both transports, from one constant. A browser copy named purchase and a server copy named Purchase are 2 events.
- Send every user field the platform will match on, on both sides. Payload composition and what each field contributes is set out in assembling a server-sent conversion a platform will match.
- Check that the identifier your visitor carries is stable within the session. A value that is regenerated on navigation breaks anything keyed on the user rather than the event, and that churn is diagnosed in returning visitors arriving as new ones.
- Do not confuse event deduplication with identity resolution. Agreeing which user this is, across 2 systems, is a different problem with different machinery — cookie sync versus server-side id mapping.
Fix at source, or suppress one transport
- Fix at source when the identifiers differ or a retry is regenerating them. Derive one identifier from the order at the moment the order is created, store it on the order row, and have both transports read it. This is a small change and it removes 3 of the 5 failure points at once.
- Suppress one transport when the gap is structural — a batch you cannot move, or a vendor integration you do not control — and you need the reported numbers correct this week. Turn off the browser copy rather than the server copy, since the server path is the one with better coverage.
- Time-box the suppression and write down the date. Running one transport permanently forfeits the resilience you moved server-side to get, and the decision quietly becomes architecture if nobody revisits it.
The suppression option is why we recommend moving one event at a time with a parallel run and a numeric gate, as in moving one conversion server-side first. An idempotent send with a deterministic identifier is also the sort of thing that costs an hour during a build and a fortnight afterwards, which is why it sits in the definition of done for the MVP and product builds we ship with measurement attached.
Before you call it fixed
- Re-run the paired report the next day. Match rate on the identifier columns should be near total, with the residual explained by consent-denied sessions rather than unexplained.
- Confirm the reported series returns to the pre-rollout level rather than to something new. If it lands 10% higher, that is the coverage the server path was supposed to add — keep the before-and-after numbers.
- Add an alert on the ratio of reported conversions to back-end orders. It is a single number, it is cheap, and it catches the next regression on the day it ships instead of at month end.
- Recheck the platform-versus-warehouse comparison afterwards, since duplication contaminates it — the ranking is in the ad platform counting more conversions than you do. The rest of this silo sits under tracking, consent and event pipelines, part of our marketing and advertising practice.
Frequently asked questions
Short answers to the follow-ups this page tends to raise.
Why are conversions duplicated after enabling server-side tracking?
Because the platform received 2 copies of the same conversion and could not tell they were the same. The commonest reason is that each transport generated its own event identifier, so nothing links them. The other 4 are a server copy arriving outside the platform's deduplication window, different event names on each side, a field the platform keys on missing from one copy, and a retry loop that issues a new identifier per attempt.
How do I test whether event deduplication is working?
Build one row per order for a completed day, carrying the browser event identifier, the server event identifier, both event names and the gap between the 2 sends. Healthy deduplication looks like identical identifiers, identical names, and a gap measured in seconds. You cannot test this from the platform's interface, because it does not show you what it discarded — the evidence has to come from your own request logs.
Should the event ID be the order ID?
Deriving it from the order identifier is the most reliable approach, either by using it directly or hashing it with the event name. The property that matters is determinism: the same conversion must yield the same identifier from both transports and from every retry. A random value generated at send time satisfies none of that, which is why it is the most common cause of duplication.
Is it safe to just turn off the browser tag?
It stops duplicate counting immediately, and it is the right emergency measure when the underlying gap is structural. It is not a good permanent state: you lose the redundancy of a second transport and any signal the browser copy contributed that the server copy cannot reconstruct. Treat it as time-boxed, write down the date you will revisit it, and fix the identifier at source in the meantime.
- deduplication
- server-side tagging
- conversions api
- diagnostics
The work behind this page
Builds from our portfolio that this page draws on.
PipelineIQ
An AI SDR platform that scores every lead for fit, runs multichannel sequences across email, LinkedIn and call, drafts the replies, and books the meeting.
Sales AIShipSight
A supply-chain control tower that tracks every shipment across ocean, air and ground, predicts each ETA with a confidence score, and flags at-risk shipments before they slip.
LogisticsRead next
- Event ID: the value that lets two systems agree a conversion happened onceThe same identifier, the same conversion, both transports, inside the platform's window. Four generation strategies, and the guarantee each one keeps or breaks.definition
- The ad account reports a third more conversions than the warehouse, every single monthFour causes, ranked. Two are defects you can fix and two are definitional differences you can only document — and an exact match is not one of the available outcomes.diagnostic
- The tagging server: a first-party endpoint that owns the payloadA tagging server is not a proxy. It parses one first-party event, adds and strips fields, and emits a vendor-shaped request per destination from your infrastructure.definition
- A slice of events lands before the visitor has answered the bannerEvents arriving with an absent consent field are not a compliance abstraction. They are a race between two scripts, and the race has a rate you can measure this afternoon.diagnostic
- Click identifiers: the URL parameters that let a server-sent conversion find the ad that caused itA campaign tag describes where traffic came from. A click identifier is the key that joins a sale back to a specific click — and only one of the two is load-bearing.definition
- Consent state: a typed field on each event, not a switch on the pageThe pageview before the banner answer and the purchase after it are both correct, and they carry different consent values. That only works if consent travels per event.definition
Working on something in this space?
Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.
Start the conversation