Skip to content
Docs

Tracking guide

Deduplication

How duplicate clicks, duplicate conversions and repeated postbacks are detected, and the settings that change the behaviour.

Dedupe is on by default and has three independent layers: clicks, conversions, and events on the wire.

Duplicate clicks

At click time the edge builds a session key and checks whether it has been seen inside the window. Per offer:

SettingValuesMeaning
session_definitioncookie, ip, ip_user_agent, gaid, idfa, click_id_onlyWhat identifies "the same visitor". The default is click_id_only, under which every click is unique; choose another definition to detect repeats. cookie is opt-in and sets a first-party cookie (dt_<offer>); ip_user_agent is the usual stateless choice; device ids suit app traffic.
unique_click_session_hourshours, default 24The window.
duplicate_click_actioncount, block, fail_trafficcount keeps the duplicate as a valid click, block and fail_traffic mark it invalid with reason duplicate.

The first click per key is flagged is_unique; later ones is_duplicate. Reports expose clicks (valid, including duplicates unless blocked), unique_clicks and gross_clicks (all, including invalid).

Duplicate conversions

When a conversion arrives, ingest claims a de-duplication key atomically (Redis SETNX), so two concurrent requests cannot both succeed:

SituationKey
txn_id presentoffer + goal + txn_id
no txn_id, click knownclick + goal
neitherunique per request (no dedupe)
offer or goal allows duplicatesunique per request

A second hit with the same key answers {"status":"duplicate"} and changes nothing. If the advertiser sends the same txn_id with a different status (and the advertiser uses a security mode other than none), the existing conversion is updated instead and the answer is updated; with security_token mode the token is re-checked.

SettingLevelEffect
is_allow_duplicate_conversionofferEvery hit creates a conversion.
is_allow_duplicategoalSame for one goal.

Several conversions from one click (for example recurring deposits) are fine as long as each has its own txn_id; without a txn_id a second conversion for the same click and goal is a duplicate. The offer fields track_multi_conversions and already_converted_action, and the goal field track_multi, are stored in the contract but are not consulted by the ingest pipeline yet.

Every new conversion's keys are also recorded in Postgres (conversion_keys) before the conversion is created, in one atomic statement, so exactly-once never depends on Redis keeping its data: if Redis is flushed, restarted without persistence or replaced by an empty replica, a resent postback finds its original there and is answered duplicate (or queued until the original is readable). If Redis or Postgres cannot answer, the conversion is queued ({"status":"queued"}) and processed once they can — never guessed as unique or as a duplicate. Redis keeps conversion state and keys for 45 days; older ones are read from ClickHouse and Postgres.

Retries after a crash

A claim remembers which ingest process holds it while the conversion is being created. If that process dies half-way (a crash, a deploy that kills it), the advertiser's request got no answer and is retried; the retry finds the claim of a process that is gone, takes it over and creates the conversion with the same conversion id — it is not answered duplicate. A retry that arrives while the first attempt is still running is answered queued and completes once that attempt has finished (or is known to be dead). A retry of a postback that was turned down because its click already converted gets the same answer again (duplicate, reason already_converted, with the id of the conversion that holds the click). Set DT_INGEST_CLAIM_OWNER=0 to switch this off (a claim is then taken over only after 30 seconds).

A queued answer is a promise: the conversion is created exactly once, under that id (or under the id of a copy of the same transaction that was queued too — find it by transaction id). Ingest never answers duplicate unless the conversion exists or the click's first conversion is known: if taking over a dead attempt's claim cannot be confirmed, the request is queued instead. How long a claim has been held is measured by Redis, so ingest servers whose clocks disagree still agree on it, and a restart lets the postbacks being processed finish before it stops.

Duplicate events on the wire

Internal events carry unique ids. NATS publishers set Nats-Msg-Id to the event id so JetStream drops redelivered duplicates, and the ClickHouse writers keep a dedupe token (tenant:id[:version]) in Redis. A status change is a new version of the same conversion; rows are append-only and the current view takes the latest version, so replays never double-count.

Outbound

Webhooks are at-least-once: de-duplicate on the envelope id. Ad-platform postbacks use the conversion id as the platform's event_id/orderId so the platform can de-duplicate against browser events. The web SDK also drops a repeated txn_id in the same browser.