Skip to main content

Transaction Queue

The transaction queue lets an integrator retry a blockchain write under the same key, so the retry attaches to the first request instead of filing it twice.
Acceptance is not settlement.A submitted write is accepted by the queue, which records intent and deduplicates any retry that reuses the same idempotency key before handing the accepted operation to durable execution. Durable execution broadcasts the transaction and retries it as needed. Only once that work finishes does a terminal settlement verdict report what happened. Acceptance happens quickly. Settlement can take longer, and the two are never the same event.Acceptance is not settlement.The same figure as the desktop variant, drawn portrait for phone widths. A submitted write is accepted by the queue, which records intent and deduplicates a retry that reuses the same idempotency key. The queue then hands the accepted operation to durable execution, which broadcasts and retries it. Only once that work finishes does a terminal settlement verdict report what happened. Acceptance happens fast. Settlement can take longer, and the two are never the same moment.

The queue's own job is acceptance and deduplication, and what settlement means once execution finishes belongs to the settlement verdict.

  • The queue deduplicates a transaction request that reaches it with the same idempotency key, sender, chain, and wallet selection within 24 hours. A route may do other work before it reaches the queue, so check that route's replay contract and the original request status before resubmitting. See Idempotent Write for the exact replay rules.

  • Acceptance answers immediately, and settlement is the answer that counts. The queue confirms a submitted write right away, and durable execution broadcasts, confirms, and reports the outcome once the chain settles it, so a fast acceptance is never read as proof the transaction happened.

  • A stalled or failed transaction is recoverable. Each signing address has its own nonce lane on each network, so one address's stuck write does not block another's. Relayed writes share the organisation relayer's lane, so a stuck relayed write holds up the relayed writes behind it. Automatic recovery resolves transient failures, and an operator can force a retry, a failure, a cancellation or a nonce correction when it cannot.

Why every write needs a queue

Every operation the platform performs against a ledger ends the same way: a signed write broadcast to a blockchain. Issuing supply, freezing a holding, paying a coupon, and settling a trade are all, underneath, one more write to a chain that charges gas whether the write succeeds or fails. A caller that resubmits a write because a connection dropped or a request timed out can turn one intended mint, transfer, or freeze into two. A doubled transfer is a real balance change on chain, with a reconciliation gap behind it. The platform signs each write with the managed key it holds for the sender's wallet, or with the organisation's relayer for a relayed write, and a write the holder signs outside the platform is signed and paid for by the holder.

More than one caller can submit work for the same sender at once. An operator working in the Console, an integrating API client, and a workflow step reacting to an earlier event can all sign against the same wallet, on the same chain, in the same minute. Every write from one sender needs a nonce, the wallet's own increasing sequence number, and two writes that reuse the same nonce from the same sender cannot both land: the chain accepts one and rejects the other. Without one place that serialises that sequence per sender and chain, concurrent callers compete for the same slot instead of queuing behind it.

Every accepted instruction needs a durable record of what was asked, what was attempted, and what the chain answered, plus a path back when a transaction stalls instead of confirming.

Why one queue for the whole platform

The alternative is each part of the platform that writes to a chain, such as asset issuance, custody, trading settlement and workflow steps, building its own retry, nonce and recovery logic. Each copy would be its own chance to double-broadcast a write or collide two nonces.

The platform instead funnels every on-chain write it signs or submits through this one queue, and a write the holder's own wallet sends is recorded on the queue for confirmation tracking. An instrument's own servicing operations queue the same way a trading venue's settled trade does. Asset Lifecycle has the full list. One deduplication rule and one ordering rule then apply the same way, whichever part of the platform asked for the write.

Acceptance is not settlement

A fast answer to a submitted write means the platform accepted it, not that the transaction happened. The queue records the intent, attaches a same-key retry to the existing request, and hands the accepted work to durable execution. What happened, confirmed, failed or something in between, is reported separately once execution finishes. Settlement Verdict explains what that terminal report means.

Once a write reaches the queue, a matching submission with the same key, sender, chain, and wallet selection within 24 hours resolves against the existing transaction request. While the work is in flight, the queue attaches to it. Once it has completed, the queue reads back its recorded result. Against a transaction that failed, dead-lettered, or was cancelled, the queue refuses the duplicate key rather than replaying it. For a dead-lettered call that may still land, the refusal says so: do not send the call again until it settles. The one exception is a failed or dead-lettered transaction that the platform rejected before signing, with no nonce, signed transaction, hash, receipt or custody provider filing recorded: the queue keeps the failed row, releases its key, and the unchanged write starts a new transaction request. The same key on a different write is refused as an idempotency conflict: HTTP 409 on most routes, and HTTP 400 with TRANSACTION_QUEUE_IDEMPOTENCY_CONFLICT on a token or token-feature write. A write that settles a one-time outcome, such as a maturity, a redemption of a holder's whole balance, a claim or a refund, holds that outcome until its transaction request finishes: a second request for it under another key is refused at once with TRANSACTION_QUEUE_OUTCOME_IN_FLIGHT, instead of being accepted and reverting on chain. An operator's force-retry of a failed request is refused the same way while a newer request for its outcome is running. This rule does not cover work an API route performs before queue admission. Check the original outcome and transaction references before starting a new attempt.

What an operator sees, and one name not to confuse it with

An operator sees the whole queue on the Transactions table of Platform status, an organisation-wide view across every wallet, not only the caller's own. It shows each transaction's status, such as Stopped after retries, the label for a dead letter, DEAD_LETTER, with its sub-status on a second line. The Submissions tray follows the requests a user started. When automatic recovery cannot resolve a stuck or failed row, an operator can step in directly rather than wait on the schedule. The levers there are the ones that What the operator API adds beyond acceptance covers.

The Console's Actions page is a separate work list for people: settlement approvals, bond maturity and redemption, votes, KYC updates, and the custody or multisig approvals a queued transaction waits on. Approving one there lets the queued request continue. A runbook about the Actions page does not apply to the transaction queue.

Ordering, delivery, and what happens when work stalls

The platform orders execution per sender address and chain ID, through that sender's nonce lane, so a pending nonce for one sender does not hold up another sender's nonce sequence. Shared network or service capacity can still delay both.

Delivery to the chain is at least once, not exactly once: the same accepted row can be resubmitted more than once over its life. The platform checks the row's state and tracks the sender's nonce to coordinate retries and replacements, as Nonce management describes. Those controls do not make an independently submitted request a safe retry. A transaction can stall with no confirmation, or it can fail in a way execution already knows does not change on retry. They end in different terminal states, by different routes and on different budgets. A stalled row is requeued repeatedly by a reconciliation sweep that runs on a five-minute cycle, and moves to Dead letter when its retries run out. A row that fails in a way retrying cannot change records Failed straight away. The recovery-path figure draws both paths, along with the one way an operator can run the row again.

Transient and terminal failures take different recovery paths.A transaction anywhere in the queue's pipeline, any state before a terminal one, can fail two ways. One that fails deterministically before it ever broadcasts, an invalid request is one case, is classified immediately and recorded as failed without spending any retry budget. One that stalls with no confirmation past a state-specific threshold is picked up by a periodic reconciliation sweep. That sweep requeues it if retries remain, or moves it to dead letter once the retry limit is reached. Failed and dead letter are terminal states on the queue's own state machine. Neither row requeues itself: only an operator's forced retry requeues it. A dead letter whose call may still land can still close as completed, failed or cancelled once the platform finds its outcome.Transient and terminal failures take different recovery paths.The same figure as the desktop variant, drawn portrait for phone widths. A transaction anywhere in the queue's pipeline either fails deterministically, an invalid request is one case, and is recorded as failed immediately with no retry spent. Or it stalls past a threshold and is picked up by a periodic reconciliation sweep, which requeues it while retries remain or moves it to dead letter once they run out. Both states are terminal, and only an operator's forced retry requeues the row.

Failed follows a deterministic rejection and Dead letter follows exhausted retries, and only an operator's forced retry runs either again, as a fresh start.

Nonce management

The transaction queue assigns every nonce itself, from one nonce lane per sender address and chain ID. Two network entries for the same chain ID share the lane. A lane has one owner at a time, and a write from an owner that another process has displaced is refused, so two processes never hand out the same nonce for the same sender.

How a nonce is reserved and sent

  1. Reserve. The lane reserves the next free nonce for the transaction and checks it against the chain's own transaction count for the sender. A reservation holds a lease of 2 minutes.
  2. Seal. The signed transaction is stored with its nonce before the platform broadcasts it. Every resend of a sealed version uses the same signed bytes, so it keeps its nonce and its transaction hash. A fee replacement, described in the next section, signs a new version for the same nonce.
  3. Broadcast. The transaction engine sends the signed bytes. On a transient node failure it sends the same bytes again, up to 3 more times, after waits of 200, 400 and 800 milliseconds.
  4. Reclaim. Once a minute, a sweep deletes reservations whose lease expired before the transaction was signed and sent, and the lane takes those nonces back.

The transaction engine's RPC client tries each endpoint once per send. When a send fails on one endpoint in a way that may have reached the node, and the client moves to another endpoint, it marks the outcome as uncertain, because the first endpoint may already have taken the transaction. How the transaction engine picks an endpoint describes that client. The engine then decides from the chain what happened instead of trusting the refusal:

The node answers
What the engine does
The transaction is already known
Keeps the transaction hash of the signed bytes and waits for its receipt.
The nonce is too low
Looks for a receipt of these signed bytes. With no receipt, it records the outcome as uncertain, because an earlier signature of the same call may have taken the nonce.
A refusal such as insufficient funds, a stale fee cap, intrinsic gas too low or an invalid configuration
Fails the attempt. It records the failure as definite only when no earlier send was uncertain and the chain does not show the transaction.

When the node refuses a transaction before it is sealed because its fee cap is stale, the engine signs it again, up to 3 times.

Stuck and dropped transactions

The engine watches every broadcast transaction for its receipt. When a transaction still has no receipt 3 blocks after the engine first saw it pending, or 3 blocks after its last replacement, the engine replaces it with the same nonce at a higher fee. It does this only for an EIP-1559 transaction with no missing lower nonce, on a network with a public mempool, with a fresh market fee quote and a chain head that is not stale. When the sender cannot pay for the replacement, the engine raises a funding request instead. Each replacement bids at least 12.5% more than the previous one, and the engine stops after 3 replacements, or when the next bid would exceed three times the original fee cap, the network's tip ceiling, the market tip envelope or the spending budget. The request settles on whichever version the chain includes.

A transaction that the watcher has polled for 120 seconds with no receipt stays Confirming and gets the sub-status TIMEOUT. Its message says which case applies: another transaction already used its nonce, the sender's pending transaction count is higher than its nonce, it is blocked behind a missing lower nonce, or nothing on the chain holds its nonce and the node most likely dropped it. A pending count higher than the nonce does not show that the transaction is still pending, because a Besu node also counts transactions queued behind a missing lower nonce. For that case the message says that the reconciliation sweep checks the transaction by its own hash and reports it as dropped when no node holds it.

The reconciliation sweep runs every 5 minutes. It checks the receipt of a transaction stuck for more than 5 minutes, then requeues it while retries remain, 3 by default, or moves it to dead letter. With no receipt, the sweep first compares the transaction's nonce with the sender's confirmed and pending transaction counts, because filing the call again while the original transaction can still be mined would run the call twice. It requeues the request only when the counts show that nothing is pending at that nonce. The rerun of a signed transaction sends the same stored signed bytes again, with the same nonce and hash, so the call cannot run twice.

When the sender's pending count is higher than the nonce, the sweep asks every configured endpoint of the network for the request's own recorded hashes: the request's transaction hash, the hash of its stored signed transaction and the hash of each recorded fee replacement or cancellation. When an endpoint knows one of them, the transaction is still pending and the sweep leaves the request alone. When every endpoint answers that it knows none of them, the sweep treats the transaction as dropped. It then holds the request on every run while the count stays higher than the nonce: it does not resend the transaction and never files the call again at a new nonce. If the count later falls back to the nonce, the slot reads as free and the sweep requeues the request, which sends the same signed bytes again. It logs Reconciliation refused to requeue a request whose nonce slot is not free with the code exec.nonce.SLOT_REFUSED and the hold_reason dropped, the metric dalp.execution.reconciliation_refused_requeues_total counts it under the reason dropped, and, once holds have continued on every run for about 15 to 20 minutes, the bundled observability stack raises the critical alert TransactionRecoveryDroppedNonceSlots. Every later transaction of that sender waits behind the dropped nonce until an operator acts. When several requests of one sender are held as dropped, act on them in nonce order, lowest first, because the chain includes a nonce only after every nonce below it. When an endpoint cannot answer the lookup and no endpoint knows a hash, the sweep holds the request as unresolved and leaves it for the next run. A run gives these lookups a 2-minute budget that only the lookups themselves spend, so an endpoint that never answers cannot hold up the rest of the run: once the budget is spent, the lookup still waiting and every later one count as unanswered, and the run logs exec.nonce.SLOT_LOOKUP_BUDGET_SPENT. A run that resumes after a long durable wait starts with a fresh budget. When such holds continue for an hour, for example because one configured endpoint keeps failing the lookup, the bundled observability stack raises the warning alert TransactionRecoveryUnresolvedNonceSlots.

The sweep also looks for a wedged lane. A lane is wedged when the sender's pending transaction count has stopped below the next nonce the lane handed out, no request holds the lowest unmined nonce, and that has lasted at least 10 minutes. Pooled transactions that sit beyond the gap do not count as work in progress, even though a node such as Besu reports them as pending. When the request that holds the missing nonce was never signed and sent, the sweep fails that request and fills the gap with a self-transfer that does nothing, so the chain can include the later nonces. A lane it cannot fill is logged as Nonce lane wedged with the code exec.nonce.LANE_WEDGED, and the metric dalp.execution.nonce_lane_wedged_total counts it. A refused fill logs exec.nonce.LANE_FILL_REFUSED. A fill that a custody policy holds for approval is refused too: the fill cannot wait for an approver, so DALP withdraws the held request. A fill that sits in the pool because the base fee rose above its fee cap is repriced on the next sweep: DALP signs the same self-transfer with a higher fee, records it as a repricing of that nonce and sends it, up to three times. A fill it cannot reprice logs exec.nonce.POOLED_FILL_NOT_REPRICED with the reason.

When a custody provider assigns the nonce itself, the lane does not hold it, and the engine cannot replace a stuck transaction on that lane. DALP cannot cancel such a transaction either. Cancel it at the custody provider.

What an operator can do

The holder of the sending wallet can cancel a transaction with POST /api/v2/transaction-requests/{transactionId}/transitions and the body {"action":"cancel"}. Cancelling a dead letter, and the fail, retry and nonce actions, need an organisation owner or a platform administrator. Before broadcast the request ends cancelled at once. After broadcast, the cancellation replaces its nonce with a zero-value self-transfer that bids at least twice the market fee, or 12.5% more than the last bid if that is higher. When the network sets a tip ceiling and a spending budget, the bid is cut to those limits and to three times the original fee cap. If even the market fee, or 12.5% above the last bid when that is higher, would break a limit, the cancellation is refused and nothing is signed. When a custody policy holds the replacement for an approver, DALP withdraws the held request and refuses the cancellation with code DALP-14035, as Custody Approval explains. An operator can also force a lane's next nonce through the same route, with the body {"action":"nonce","nonce":42,"reason":"Align the lane with the sender's on-chain count"}. A forced nonce needs a reason, and the lane keeps the last 10 forced values with their reason and time. The Platform API reference documents both operations.

A transaction that the sweep holds as dropped leaves the operator two ways out. The first is to rebroadcast the request's signed transaction with eth_sendRawTransaction. No Platform API returns it, so the deployment operator reads it from the platform's database. When the request has a recorded fee replacement, send the latest replacement, which bids the highest fee. When the request has a recorded cancellation, someone cancelled it: do not rebroadcast the call, cancel the request again, and escalate to engineering if that cancellation is refused. A write that a custody provider signed can have no stored signed transaction, so act on the custodian's record of the request instead. The sweep does not rebroadcast a transaction it holds as dropped. The confirmation watcher can: when the request has a recorded fee replacement, it sends the latest replacement again, and on a network with a public mempool it can replace the transaction at a higher fee. The rebroadcast keeps the original nonce and hash, so the call cannot run twice, but it can land after the caller stopped waiting. Before you rebroadcast, confirm that the caller has not already submitted the same action again under a new idempotency key. The second way out is to cancel the request, which fills the nonce with the cancellation's self-transfer. A node outside the network's configured endpoints, such as a validator, can still hold the dropped transaction, so check that node's pool before you cancel. Force-fail and force-retry, the fail and retry actions of the same route, refuse a dropped request that has signed or broadcast its transaction with an HTTP 409 conflict whose message names the ways out, including the rule never to rebroadcast a request that has a recorded cancellation.

What each outcome leaves you

A transaction has four terminal states. Any other state, such as QUEUED or CONFIRMING, is still in progress, including after a synchronous wait ends, which only means the wait budget ran out. Execution continues. Completed means the chain settled the work with a recorded outcome. Failed means execution rejected the transaction outright or hit a failure that retrying cannot fix. Dead letter means recovery stopped without a final result, usually because the retries ran out or another call took the nonce. A call that was sent, and whose nonce no other call took, may still land, and so may a call filed with a custody provider. The request's mayStillLand field says which: true while the call can still land, false once it cannot. Cancelled records a stop the requester asked for, before execution finished.

What you see
What is still possible
Still pending after your synchronous wait
Read back the status URL the response carried, until the transaction reaches a terminal state. The queue deduplicates a matching submission, but repeat the API request only when its route documents replay safety.
Completed
Nothing. The row stays closed, and the platform blocks a forced retry against it.
Failed
An operator can force a retry. You can submit a fresh attempt yourself under a new idempotency key. The row does not requeue itself.
Dead letter
If the call may still land, the refusal says so: do not send it again, because the platform keeps checking it and reports the outcome when it settles. Ask an operator to close it after a chain or provider check. When mayStillLand reads false, submit a fresh attempt under a new idempotency key. The row does not requeue itself.
Cancelled
The platform blocks a forced retry. A fresh attempt needs a new idempotency key.

Between force-retry and a fresh attempt, the choice turns on what you have already confirmed. A forced retry keeps the row's identity, its idempotency key included, so it is the right lever when you want the same logical operation to continue under its original name. Only an operator can pull it. Before using a new key, read the original terminal state and inspect its transaction references if the chain outcome is uncertain. Do it earlier and the new key broadcasts independently, even if the original request already succeeded: exactly the hazard the FAQ on resubmitting under a new idempotency key walks through.

A forced retry is not a resume. It requeues the row from the start, clears the recorded error, and resets the retry count, so the row gets a full budget again rather than the remainder of a spent one. That is why it is an operator's decision rather than an automatic one: whatever exhausted the first budget is usually still true.

Nothing about any of this rewrites the row's past. The platform appends each forced transition to the row's own history, so the trail shows the state it came from and the reason the operator gave, and a reopened transaction stays visibly a reopened transaction.

What the operator API adds beyond acceptance

The queue admits one accepted instruction per sender, chain, and idempotency key. A second submission of the same write under the same triple is not an error: it signals that the work already exists, so the queue attaches it to that work rather than broadcasting again. A different write under the same triple, or a key first used more than 24 hours earlier, is refused as an idempotency conflict. The queue follows the exact replay rules in Idempotent Write. Once accepted, asynchronous is the default: the call returns immediately with a handle rather than waiting on anything. A caller can opt into a bounded synchronous wait instead, clamped by whatever time the request has left. Past that budget, or by default, the response degrades to an async-accepted handle, the transaction's identifier and a status URL to read it back with, rather than failing the call. A synchronous caller that hits its budget receives a pending result, not a rejection. The exact wait budget and its limits are in the API reference.

The platform classifies deterministic validation or contract failures separately from temporary faults. A recognised gas, account prefund, or paymaster-deposit shortfall is excluded from terminal classification so the funding flow can pause and retry after a top-up. Recovering a stuck, failed, or dead-lettered row outside its automatic recovery is an operator-only step: a force-retry, a force-fail with a recorded reason, a cancel, or a force-nonce correction, each scoped to exactly the failure mode it exists to fix.

What a stalled or failed transaction does not mean

A failed or dead-lettered transaction is not a lost transaction. Its row and its full history persist and stay visible in the operator-facing transaction table until someone acts on it. Reaching either state does not delete evidence of what the caller attempted. A queue timeout on a synchronous call is not a rejection either. The work continues under durable execution, and the platform hands the caller a handle to check later rather than a definitive failure. Reporting either of these as a hard failure overstates what the platform recorded.

The platform scopes a wallet-balance shortfall error to the caller's own resolved executor wallet, never a shared Operator Wallet funded by the platform itself and never another participant's address. That scoping is what keeps a routine gas-shortfall report from becoming an unintended disclosure of a different party's wallet identity.

Frequently asked questions

Does resubmitting under a new idempotency key ever create an unwanted duplicate?

Yes. The queue's protection covers a repeated key against the same sender and chain. The platform treats a new key as a new intent, not a retry, so it broadcasts on its own even if the original request already succeeded. If a caller mints a new key without first reading back the original transaction's status, the platform has no way to recognise the two as the same work, and both proceed.

Does the same idempotency key protect a retry across two different chains?

No. The queue deduplicates on the sender, the chain, and the idempotency key together. The same key on a second network is a different triple, so it starts an independent accepted instruction there rather than attaching to the first network's work. A caller retrying one logical operation across more than one network needs a separate key for each, matching the way the platform already scopes ordering.

Does forcing a retry give a failed transaction a new idempotency key?

No. A first force-retry applies only once a row has reached Failed or Dead letter. A still-stalled row keeps recovering on the automatic schedule instead. A Dead letter row whose mayStillLand reads true can run twice if retried. When it has no nonce reservation, force-retry reads the slot and refuses with DALP-200383 unless no submitted nonce is recorded, the chain does not show the call, and the operator independently confirms that the original call was stopped, with the provider for custody calls, by sending custodianStopped: true. DALP records this operator attestation. It does not verify the stop. The confirmation never overrides a visible call or an unresolved nonce slot. A provider filing without a hash or a recorded nonce also needs this confirmation. Signed nonce reservations keep their settlement checks. The platform records the confirmation with the retry and refuses a request that changed during its check. A step of a role set is never retried this way, because it would land alone with no role check, undo or expiry. A new role set repeats the change. If the retry requeued the row but could not start execution, repeating force-retry on that unclaimed Queued row resumes its attempt or starts a new attempt if the earlier run ended. What it does is requeue the same row rather than create a new one, so the row keeps its original sender, chain, and idempotency key. Everything about its execution resets: state, retry count, recorded error, revert and contract or RPC error detail, nonce, signed transaction, hash, confirmation count, and receipt. A gas price in the operator's request replaces the stored one. See Idempotent Write for the wallet-selection fingerprint a requeued row still carries forward unchanged.

Why can an operator reopen a failed transaction but not a completed one?

Completion means the chain accepted the work, so reopening a completed row would mean broadcasting a second transaction for something that already happened, which is exactly what the queue's replay rules exist to prevent. A failed or dead-lettered row can be requeued after its cause is resolved, except a dead letter whose mayStillLand reads true. Inspect the recorded transaction and chain receipt first when the actual outcome is uncertain.

Was this page helpful?