Back to notes

September 30, 2026 · 6 min read

Conversions API Deduplication Is Harder Than the Docs Admit

Meta's CAPI deduplication guide makes it sound like matching an event_id solves the problem. In practice I've spent entire sprints on edge cases it doesn't mention.

CAPIEngineering

Meta's deduplication docs make it sound like a solved problem: generate an event_id, send it with both the browser pixel event and the server-side CAPI event, Meta matches them, done. The first time I actually implemented this at scale, I hit three separate edge cases that documentation doesn't mention, each of which produced either double-counted or entirely dropped conversions.

The edge cases that actually bite

  • Event timing matters more than the docs imply — if the server-side event arrives meaningfully before the browser pixel event, in some observed cases deduplication matching degrades, especially under high event volume.
  • The event_id has to be generated once and passed through unchanged across every system that touches the event — a surprisingly easy thing to break when a server-side enrichment step regenerates an ID instead of passing one through.
  • Multi-step checkout flows where a purchase event can legitimately fire twice (a retry after a payment gateway timeout, for instance) need application-level idempotency, which dedup by event_id alone doesn't solve if the retry itself generates a new ID.
  • Testing dedup locally is nearly impossible — Meta's event matching behavior isn't something you can fully verify outside production traffic, so bugs here surface in live campaign data, not in a test environment.
The documentation describes deduplication as matching two events. In production, it's matching two events that were generated by two different systems, at two different times, under two different failure conditions, and expecting the same ID to survive all of it.

What actually worked

Client event fires
   → generates event_id (UUID) once, client-side
   → sent to Meta Pixel immediately
   → same event_id pushed to data layer
   → server container reads event_id from data layer, never regenerates
   → server event sent to CAPI with identical event_id

Retry logic: check idempotency key (order_id) before resending,
not just event_id uniqueness.

The fix that actually held up in production was separating two different concepts that the Meta docs conflate: event_id for Meta's own dedup matching, and a business-level idempotency key (usually an order ID) for our own retry logic, checked before anything even reaches the point of generating a new event_id. Once those were separate, the weird double-counting cases disappeared, because we stopped relying on Meta's matching to cover a problem it was never actually designed to solve.