Where does it stop?
The node accepted your transaction and gave you a hash. It waited in the mempool, and then another transaction took its place. It will never reach a block.
What happened to my transaction?
An account’s transactions are numbered by nonce, and each number can be used once. Two transactions with the same sender and nonce can wait at the same time, but only one of them can be mined.
On Etherscan the first one shows as “Dropped & Replaced”. Its gas fee is not charged, because it was never mined.
Who replaced it?
Open the replacing transaction: same sender, same nonce. What changed tells you why. viem uses the same three cases when it notices a replacement (onReplaced):
viem reason |
What changed | Usually |
|---|---|---|
repriced |
Only the fee | A speed-up |
cancelled |
Value is 0 | A cancel |
replaced |
Value or data | A different transaction |
repriced and cancelled are usually you or your wallet. replaced is the one to check: another process signed with the same key.
Why does it happen?
- You sped it up or cancelled it in a wallet. This is the expected case.
- A retry sent a new version with a higher fee while the first was still waiting.
- Two senders share one key. Two workers or two agents each pick the next nonce and send. The second one replaces the first if it pays enough more; otherwise the node refuses it with replacement transaction underpriced.
What should I do?
- Check the replacing transaction’s status. If it succeeded and it was your speed-up or cancel, you’re done.
- If you still need the original action, send it again with the next nonce. The old one can’t be revived.
- If you didn’t expect a replacement, find the second sender. Give each worker its own key, or let one place assign nonces.
- In code, handle the replacement instead of waiting forever:
import type { Hash, PublicClient } from 'viem';
declare const publicClient: PublicClient;declare const hash: Hash; // the transaction you are waiting forconst receipt = await publicClient.waitForTransactionReceipt({ hash, onReplaced: ({ reason, replacedTransaction, transaction }) => { console.warn(`${replacedTransaction.hash} ${reason} by ${transaction.hash}`); },});// in our run (wait started first): receipt.transactionHash was the replacing transaction's hashIn our run, when the wait had read the first transaction before the cancel arrived, it resolved with the receipt of the transaction that replaced it. Check receipt.transactionHash before you treat it as yours.
Why is my wait stuck after a replacement?
In our runs, viem spotted a replacement only when it had read your transaction before it was replaced. When the cancel went out right away, the wait never saw the original: it ran until viem’s default timeout of 180 seconds and threw WaitForTransactionReceiptTimeoutError. With the cancel sent 500 ms later, the same wait resolved with the cancel’s receipt.
So a wait that hangs after a speed-up or cancel may not be stuck on the chain. Look the nonce up instead: if getTransactionCount for the sender is past your nonce and your hash has no receipt, it was replaced. hashspan ends that confirm span with error status and error.type timeout.
What does it look like in a trace?
hashspan ends the confirm span of the waited-for hash as replaced, with the hash that took its place. The receipt goes on the replacing transaction’s own confirm span:
- blockchain.tx.status
- replaced
- blockchain.tx.replacement.hash
- the hash of the replacing transaction
So the fee and the status are never put on the wrong hash. How a receipt is attributed is in the transaction lifecycle.