§ 03 · Concepts
Event deduplication
How event_id dedup works across Meta, TikTok, Google Ads, and Pinterest.
Event deduplication
If your client pixel and server-side CAPI both fire, you'll double-count conversions unless every event carries a stable event_id. Here's the mechanics.
Deduplication is not a TrackLayer-specific trick. It is the basic contract every hybrid setup relies on. Your browser pixel sees the action in real time, and your server confirms the same action with richer identifiers and stronger delivery guarantees. That gives you better resilience, but it also creates a new failure mode: if the browser and server versions of the event do not look like the same event to the downstream platform, the ad network keeps both.
That is why the implementation detail that matters most is not the access token, the API path, or the retry policy. It is the identifier discipline around one business action. For purchases, this is usually your order ID. For a lead, it might be the CRM submission ID or a checkout attempt ID. What matters is that the identifier is created once and then reused everywhere the same event is emitted.
The 3 pieces of dedup
Most platforms need three signals to decide whether two incoming events represent the same action:
event_idmust be stable across client and server firings for the same event. Usually your order ID.event_namemust match the action type, such asPurchase,Lead, orCompleteRegistration.event_timemust land within roughly 60 seconds between the client and server copies.
The first point is the big one. If the browser fires Purchase with event_id = order_12345, the server must send Purchase with the same event_id. If the browser uses order_12345 and the server uses 9f4f0a44-..., the platform will treat them as different conversions even if every other field matches.
The second point is easy to miss during refactors. Teams sometimes let the browser keep Purchase while a backend service emits OrderCompleted. Humans know those mean the same thing. Platforms do not. Dedup logic compares exact event types, not your intent.
The third point matters because ad platforms protect themselves from overly broad merging. If two events arrive minutes apart, they may assume they are different actions and keep both. That is why you should prefer the original event timestamp from the browser or checkout completion moment, not the later time when your backend worker finally dequeued the message.
Here is the shape you want for a paired browser and server purchase:
// Client side (Meta Pixel)
fbq('track', 'Purchase', {
value: 129.99,
currency: 'USD',
}, {
eventID: 'order_12345', // <- stable event_id
});
// Server side (TrackLayer SDK -> CAPI)
await tl.track('Purchase', {
event_id: 'order_12345', // <- same value
occurred_at: new Date().toISOString(),
user_data: { email, client_ip, user_agent, fbp, fbc },
custom_data: { value: 129.99, currency: 'USD' },
});This example is intentionally boring. That is the goal. Dedup works when the event is predictable, not clever.
How Meta dedupes
Meta's deduplication logic is the most commonly discussed because hybrid Meta setups are common and double-counts are obvious in Events Manager. In practice, Meta coalesces events with the same (pixel_id, event_id, event_name) within a time window. If all three line up, Meta can decide the browser and server copies describe the same underlying action.
The important nuance is time. If the client event fires at checkout completion and the server event is stamped much later because you used the API receive time or queue processing time, Meta may keep both. The rule of thumb is about 60 seconds. That is not a formal SLA you should code against, but it is a useful operational threshold. Once you drift outside that window, dedup becomes less reliable.
The practical fix is simple: pass occurred_at to the server-side call using the event time from the original action, not the time the backend happened to process it. If your order service writes an event to a queue and a worker forwards it 90 seconds later, the worker should still reuse the checkout timestamp. Otherwise Meta sees one browser event from now and one server event from later, and your reporting starts to inflate.
Also remember that Meta compares the event name exactly. Purchase and purchase are not the same thing operationally if your client and server use different conventions. Normalize the name once and keep it identical across every firing path.
How TikTok/Google Ads/Pinterest dedupe
The other major platforms follow the same pattern with slightly different keys.
TikTok uses event_id together with the effective advertiser scope and event name. In practice, think of it as event_id + (advertiser_id, event_name). If the same advertiser receives the same event name with the same event_id, TikTok has the signals it needs to collapse the browser and server copies.
Google Ads is less straightforward because click identifiers still dominate attribution. gclid is the primary key in many Google Ads conversion flows. event_id acts more like a secondary dedup signal, and Enhanced Conversions can also dedupe against a hash of the order or transaction context. The implementation lesson is not to abandon event_id; it is to keep both. Preserve gclid when available, and still send a stable event identifier so retries and hybrid instrumentation do not create duplicate conversions around the same order.
Pinterest is simpler. It deduplicates using event_id within the same ad account. That means the account boundary matters, but the underlying operational rule remains the same: create one durable ID per business action and reuse it across channels.
Across all three, the pattern is consistent. Dedup is not magic inference. It is deterministic matching over a small set of fields.
Anti-patterns
The most common dedup bugs are self-inflicted:
- Using
Date.now()as theevent_id. Every retry becomes a brand new conversion because the ID changes on every send. - Re-generating
event_idon the server when it was already set on the client. Meta sees two different events because that is exactly what you told it. - Using
session_idinstead ofevent_id. Sessions span multiple actions, so distinct purchases or leads start colliding with each other.
A few more subtle failures show up in production. One is deriving the event_id from something unstable like a cart token that changes after payment authorization. Another is firing the browser pixel before the order record exists, then inventing a different ID after the order is committed. If your flow does that, create an action ID earlier and carry it through to order creation rather than swapping identifiers midstream.
Retries are another source of confusion. A retry is supposed to be the same event. That means the event_id must stay the same on every retry attempt. If your retry middleware wraps the payload and regenerates identifiers, you have built a duplication machine.
How TrackLayer helps
TrackLayer removes most of the implementation friction, but it cannot invent clean identity rules if your application does not have them.
The SDK auto-generates an event_id if you do not pass one, using UUID v4. That is useful for one-off server-only events and safer than leaving the field blank. But for business-critical conversions such as purchases and qualified leads, you should still pass your own durable ID so retries and browser-side instrumentation stay anchored to the same action.
If you use our client-side helper script, it reuses the event_id from the page if the event already fired in the browser. That reduces the odds of the classic bug where the client emitted one ID and the server emitted another a few milliseconds later.
The dashboard also includes a Dedup Checker tool that shows Meta pixel and CAPI event counts side by side so you can see whether dedup is actually working. That matters because logs can look healthy while reporting is still inflated. Side-by-side counts make the mismatch visible quickly.
In other words, TrackLayer can preserve, propagate, and inspect the dedup signals. Your job is to provide a stable business identifier at the moment the event is born.
Still seeing double-counts? Run the Dedup Checker at /tools/dedup-check and look for the CAPI fingerprint.