DEV Community

Christian Pichichero
Christian Pichichero

Posted on

A Transaction Timeout Is Not a Transaction Failure

You broadcast a transaction, wait for the RPC response, and get a timeout. What happened?

The honest answer is that you do not know. The node may never have received the request. It may have accepted the transaction into its mempool but lost the response. Another node may already have propagated it. The transaction may even be mined while your executor records a network error.

Treating that timeout as a failure and sending a fresh transaction can execute the same business action twice.

Separate the request from the transaction

An RPC request and a blockchain transaction have different lifecycles. A request can fail at the HTTP layer while the transaction continues through the network.

This distinction produces three outcomes, not two:

  • Succeeded: chain state shows the intended transaction was included.
  • Failed: chain state shows a terminal failure, such as a reverted transaction.
  • Uncertain: the executor cannot yet establish either outcome.

uncertain is not a softer spelling of failed. It is a durable state that needs reconciliation.

Persist the intent before broadcasting

The first durable record should describe the business action, not the RPC attempt. Write it before signing or sending anything.

A minimal record might look like this:

intent_id:      rebalance:account-42:2026-09-03T10
account:        0xabc...
chain_id:       8453
operation:      swap token A for token B
status:         created
nonce:          null
signed_tx:      null
tx_hashes:      []
Enter fullscreen mode Exit fullscreen mode

Give intent_id a uniqueness constraint based on the business operation. If two workers receive the same job, they should load the same intent rather than create independent transactions.

Next, reserve a nonce, construct the transaction, sign it, and persist the signed bytes and hash before broadcast. This ordering lets a restarted worker send the exact same payload.

A database entry marked broadcasting does not prove that any node saw the transaction. Likewise, a missing sent log does not prove that no node saw it. Your database records what the executor attempted; the chain records what took effect.

Use nonces deliberately

For an EVM externally owned account, the nonce is the main duplicate-execution boundary. Two transactions from the same account with the same nonce cannot both become canonical.

That does not mean an executor can derive any nonce from an intent ID. Account nonces are sequential. A practical nonce manager must serialize reservations per account and persist them so concurrent workers do not choose the same nonce for unrelated intents.

After a timeout, rebroadcasting the exact signed bytes is the simplest safe retry. The transaction hash stays the same, and nodes that already know it can treat the broadcast as redundant.

Fee replacement is more complicated. A replacement uses the same nonce but changes fee fields, producing a new hash. Persist every candidate hash under the same intent and monitor all of them. Never respond to a timeout by allocating a fresh nonce to the same action.

If the original transaction has already been mined, a replacement may be rejected because the nonce is too low. That error is a reason to inspect chain state, not to create another transaction.

Smart-contract accounts can provide a stronger application-level boundary by storing an operation ID and rejecting duplicate execution. That handles cases an account nonce cannot express, but it adds contract complexity and execution cost. Sequential EOA nonces are simpler, though they can become a throughput bottleneck.

Reconcile with chain state

A reconciliation worker should examine evidence in roughly this order:

  1. Check receipts for every transaction hash attached to the intent.
  2. Check whether the sending account's nonce has been consumed.
  3. Inspect the destination contract's state or events for the intended operation.
  4. Wait for the confirmation depth required by the application before marking the intent final.

A single RPC node saying “not found” is weak evidence. Mempools differ, nodes can lag, and old pending transactions may disappear from local views. Querying another provider can improve visibility, but it still does not turn absence into proof.

If the account nonce has advanced, determine which transaction consumed it. Your intended transaction may have been included, replaced, or displaced by another process using the same account.

Reorganizations also matter. included and finalized should be separate states. An included transaction can temporarily disappear and need further reconciliation.

The ugly middle state

Suppose the broadcast timed out, no receipt is visible, the transaction is absent from the RPC node, and the account nonce has not moved. The transaction might still be propagating, or every node might have dropped it.

There is no honest terminal answer yet.

Keep the intent uncertain, rebroadcast the same signed payload, and schedule another reconciliation pass. Time alone should not silently convert uncertainty into failure. If the application eventually stops retrying, preserve the unresolved status for operator inspection.

A “cancel” transaction is also only a competing transaction with the same nonce. It can win the race, or the original can be mined first. Model the outcome from chain evidence rather than from the fact that a cancellation request was sent.

Disclosure: I build Tradevo, which compiles plain-English crypto strategies into deterministic rules, runs historical tests and paper trading, lets creators submit strategies for human review and publish approved strategies to a marketplace, and lets subscribers run published rules from wallets they control.

A useful executor invariant

The core invariant is simple:

One business intent may have several broadcast attempts and transaction hashes, but it must not silently become several independent nonce-bearing actions.

That requires coordination across the database, signer, nonce manager, broadcaster, and reconciliation worker. Idempotency is not a retry flag attached to an RPC call. It is a state machine built around the possibility that the network acted while your process learned nothing.

Top comments (0)