Where duplicates come from
Four causes produce almost every duplicate:
None of these are faults in your code. They are properties of distributed systems, and the reference is the defense against all four.
Design a reference
Make references unique, stable, and readable.- Generate the reference before the first attempt, and store it with the intent.
- Include a type prefix, such as
payout_,trade_, orcard_. - Include enough entropy to avoid collision, such as a date and a short random suffix.
- Reuse the same reference for every retry of the same business action.
1689731218712 or a bare UUID satisfies uniqueness but tells you nothing in a log. A readable prefix pays for itself during an incident.
Deduplicate on your side
We enforce the reference. You still need your own check, because your system also processes each event. Record the key before you act, and check it before you act again. For a webhook, the key isevent_id. Several events can share one reference, so don’t use reference there. For your own requests and jobs, the key is the reference.
- Your webhook handler, using
event_id. - Your user-facing send or payment endpoint, using the reference.
- Your asynchronous job processors, using the reference.
Store references in a dedicated table
A dedicated table gives you a fast uniqueness check, an audit trail, and the data behind a replay tool.
Lock across concurrent workers
A check followed by a write is not atomic. If two workers run the check at the same moment, both pass it. Take a lock, either in your database:SET ... NX succeeds only if the key is absent. Treat a failed SET as “another worker owns this reference”.
Where references appear
Send a reference on every endpoint that moves value: The reference comes back on the object and on the webhook event. That is what lets you match our record to yours.Handle a duplicate response
What a reused reference does depends on the endpoint:
In every case, generate the reference before the first attempt and never mint a new one for a retry. A UUID is fine, as long as you store it against the transaction before the first attempt. See Errors and retries.
Worked example
A user requests a payout. Your service does the following:1
Create and store the reference
Generate
payout_20260408_a41f9c and write it to your database with status pending.2
Submit the payout
Call Initialize payout with that reference. The call times out.
3
Retry with the same reference
Submit the identical request. We recognize the reference and return the original result. Nothing executes twice.
4
Receive the webhook
We send
payouts.withdrawal.success carrying the same reference and a new event_id. Your handler checks the event_id, finds it unprocessed, and marks the payout delivered.5
Receive the webhook again
A retried delivery arrives with the same
event_id. Your handler finds it already processed and returns 200 without acting.