Imagine a user requests a cryptocurrency withdrawal from an exchange. The backend processes the request and broadcasts the transaction to the blockchain, but the API response times out. Assuming the withdrawal failed, the user clicks “Retry.” If the exchange creates a new transaction without checking the original request, the same withdrawal could be processed twice.
This is a critical problem for crypto exchanges, where duplicate payouts can cause financial losses, inconsistent wallet balances, and difficult reconciliation processes. The challenge is that a failed API response does not necessarily mean the underlying transaction failed.
Keeping duplicate withdrawals from slipping through in crypto exchanges isn’t a one-and-done fix. It calls for safeguards across the whole transaction from start to finish. Idempotency keys stop the same withdrawal from firing off twice through the API, and solid database rules plus atomic fund reservations guard against multiple requests arriving at the same time.
Idempotency is a key part of this solution. Combined with database constraints, atomic fund reservations, and blockchain transaction tracking, it allows an exchange to handle retries without creating duplicate withdrawal operations
How Duplicate Crypto Withdrawals Happen in Production
Duplicate withdrawals can occur at several stages of the withdrawal lifecycle.
API timeouts after successful processing
A server may process a withdrawal successfully but fail to return a response before the client times out. The client cannot determine whether the operation completed, so it submits another request.
Blockchain broadcast failures
An exchange sends a signed transaction to an RPC provider but does not receive confirmation that the submission succeeded. Retrying with a newly created transaction may result in a second payout if the original transaction was already accepted.
Concurrent requests
A user double-clicks the withdrawal button, or two requests arrive almost simultaneously. If both requests pass validation before either is recorded, the backend may process them independently.
Automatic retries and worker failures
HTTP clients, API gateways, and background workers can retry operations after timeouts or crashes. A worker might broadcast a transaction and crash before saving its status, leaving the next worker unaware that the transaction may already exist.
These scenarios share one underlying problem: the system cannot reliably distinguish a repeated attempt at the same withdrawal from a genuinely new withdrawal request.
Why Traditional Retry Logic Is Not Enough
Retries improve reliability, but blindly repeating financial operations introduces risk.
An HTTP timeout only indicates that the caller did not receive a response within the expected period. It does not establish whether the server processed the request or whether the blockchain accepted the transaction.
Similarly, checking the user's available balance is insufficient when multiple requests execute concurrently. Two requests could both observe the same balance and attempt to withdraw the same funds.
Database transactions also cannot automatically guarantee that an external blockchain operation and an internal database update succeed together. They are separate systems with different failure conditions.
A safe withdrawal architecture must therefore distinguish three outcomes:
- Request failure: The API request did not complete as expected.
- Broadcast uncertainty: The exchange cannot determine whether a transaction was submitted.
- Settlement failure: The transaction failed according to the relevant blockchain or business rules.
Each outcome requires different recovery behavior. Treating every timeout as a failed withdrawal can turn a temporary network problem into a duplicate payout.
Implement Idempotency Keys for Withdrawal APIs
Idempotency handles repeated submissions of the same logical operation as if only one operation was ever submitted.
Generate and store an idempotency key
A unique key is generated for each intended operation (e.g. withdrawal) by the client.
The backend stores this key together with the corresponding withdrawal operation for the respective user or account. When a client initiates a withdrawal, it supplies a unique key identifying that intended operation.
For example:
POST /api/v1/withdrawals
Idempotency-Key: wd_8f2a91c7
Content-Type: application/json
{
"asset": "USDT",
"network": "Ethereum",
"destination": "0xRecipientAddress",
"amount": "100.00"
}
If the client retries because the response was lost, it must reuse the same key.
The backend should return the existing withdrawal record or its current status instead of creating a second withdrawal.
Validate repeated requests
An idempotency key must not allow different withdrawal instructions to be treated as the same operation. Store a request fingerprint covering relevant fields, including the asset, network, destination, and amount.
If the same key arrives with different parameters, reject the request rather than silently changing the existing withdrawal.
The backend should also enforce uniqueness for the appropriate key scope, such as the user account and idempotency key combination.
Most importantly, idempotency must be enforced server-side. Generating a key in the frontend alone does not prevent duplicate processing
Prevent Race Conditions With Atomic Database Operations
Idempotency keys work best when supported by database-level safeguards.
Enforce uniqueness
Create a unique constraint on the idempotency key and its scope so duplicates can’t spawn multiple withdrawals for the same context. If two requests race to do the same action, only one will end up creating the record. The other one should gracefully fetch the existing record and return its current status.
Reserve funds atomically
The exchange should validate the available balance and reserve the withdrawal amount within a controlled database transaction.
For example, a withdrawal of 100 USDT should reduce the amount available for other withdrawals before asynchronous processing begins. This prevents concurrent requests from reserving the same funds.
The reservation and withdrawal record must be persisted consistently. If the database transaction fails, the system should not leave a partially created withdrawal.
Funds should only be released when the system has established that the withdrawal cannot still complete. An uncertain blockchain outcome requires investigation rather than immediate release.
Use conditional state transitions
Workers should update a withdrawal only when its current state permits the transition. Conditional updates, row-level locks, or optimistic concurrency controls can prevent multiple workers from claiming the same operation.
These safeguards ensure that duplicate requests cannot bypass the business rules simply because they arrive at the same time.
Design a Retry-Safe Withdrawal State Machine
A withdrawal state machine makes recovery behavior explicit.
A typical lifecycle might include:
- REQUEST_RECEIVED: Validate the request and idempotency key.
- FUNDS_RESERVED: Reserve the amount and persist the withdrawal.
- BROADCAST_PENDING: Prepare and submit the signed transaction.
- CONFIRMATION_TRACKING: Monitor the transaction and reconcile its status.
- CONFIRMED: Finalize the withdrawal according to the exchange's confirmation policy.
- FAILED or MANUAL_REVIEW: Handle definitive failures or unresolved outcomes appropriately.
Each transition should have clear conditions. For example, a withdrawal should not return to its initial state simply because a worker times out.
The state machine must also distinguish a transaction that has never been submitted from one whose submission result is unknown. The latter should trigger transaction recovery and reconciliation, not automatic creation of a new payment.
The exact states and finality requirements depend on the blockchain network and the exchange's custody architecture.
Handle Blockchain Broadcast Retries Without Sending Another Payment
The toughest recovery happens when a transaction might’ve made it onto the blockchain, but the exchange never gets a solid confirmation.
Persist transaction data before broadcasting
Before sending the signed transaction, its computed transaction hash, and its corresponding withdrawal ID to the RPC provider, if the transaction architecture allows it.
This gives the system a durable reference for recovery if a worker crashes or a network connection fails.
Check the original transaction before retrying
For EVM-compatible networks, the eth_sendRawTransaction JSON-RPC method accepts a signed transaction. If the submission response is lost, the exchange should investigate the original transaction hash and relevant blockchain state before preparing another transaction.
If the original signed transaction is available, resubmitting that same transaction is different from creating a new payment with a different nonce or transaction payload.
Errors such as “already known” or “nonce too low” should trigger investigation. They do not, by themselves, prove that the intended withdrawal has completed successfully.
Handle transaction replacements carefully
A pending transaction may sometimes require a fee replacement. However, the replacement must remain linked to the same logical withdrawal and follow the exchange's transaction-management rules.
The goal is to replace or recover the original transaction where appropriate, rather than creating an extra transfer.
These principles must be adapted to each chain. Account-based networks, UTXO-based networks, and third-party custody providers have different transaction models and recovery mechanisms.
Make Background Workers and Message Queues Idempotent
Withdrawal processing often involves queues and asynchronous workers. Because jobs may be delivered more than once, consumers must be designed to handle repeated execution safely.
A transactional outbox can help coordinate database changes with job publication. The withdrawal record and outbox event are written in the same database transaction, allowing a publisher to deliver the event later without losing the operation.
Employees should utilize atomic claims, conditional updates, or leases to coordinate processing.
They should also record broadcast attempts and outcomes so that another worker can recover an interrupted operation.
Consider a worker that broadcasts a transaction and crashes before updating the database.
When the job is delivered again, the next worker should retrieve the original transaction details and reconcile its status rather than generate a fresh payment.
Queue deduplication is useful, but it is not sufficient by itself. A queue cannot guarantee that an external blockchain transaction was executed only once. The withdrawal service must enforce its own idempotency rules.
Maintain Ledger Consistency and Reconcile Withdrawals
Stopping duplicate requests is only one piece of keeping finances honest. The exchange also has to make sure its internal ledger lines up with how withdrawals actually happen.
Each logical withdrawal should have a traceable relationship with its ledger entries, fund reservation, transaction hash, and settlement status.
The system should track available, reserved, and settled balances separately. This makes it easier to determine whether funds are awaiting broadcast, associated with a pending transaction, or ready for final settlement.
Reconciliation processes should compare internal withdrawal records with blockchain transaction data. They should identify missing transactions, unexpected transaction hashes, inconsistent statuses, and mismatched ledger entries.
A missing transaction receipt does not necessarily mean a transaction was never broadcast. Depending on the chain and circumstances, the transaction may still be pending, dropped, replaced, or affected by a reorganization.
When the outcome remains uncertain, the exchange should preserve the relevant records and escalate the case for further reconciliation instead of automatically releasing funds or initiating another payout.
Test Duplicate Withdrawal Prevention Before Production
Testing should cover failure scenarios across the API, database, workers, and blockchain integration.
Use integration tests and failure injection to simulate crashes immediately before and after database commits, job publication, and transaction broadcasting.
Monitor operational metrics such as idempotency conflicts, repeated broadcast attempts, unresolved withdrawals, reconciliation mismatches, and duplicate ledger operations.
These checks help identify weaknesses before they affect customer funds.
Conclusion
The uniqueness of requests is only one aspect of financial integrity. The exchange must also verify that its internal ledger matches the actual withdrawal journey.
A solid state machine, saved transaction data, and background workers that retry safely, along with reconciliation against the blockchain, keep recovery safer when the network hiccups or a worker crashes and the withdrawal outcome isn’t clear.
Bottom line: a missing response isn’t proof that the withdrawal failed. The system should first figure out what happened to the original operation before deciding what to do next.
If your withdrawal API times out immediately after a transaction is broadcast, can your system prove that retrying the request will not send the funds twice?


Top comments (0)