A transaction is pending forever and blocks the rest
“One transaction never confirms, and every later transaction from the same account sits behind it.”
ERROR CODES YOU MAY SEE
A code is a clue. Use its meaning and surrounding evidence to narrow the cause.
REPLACEMENT_UNDERPRICEDethers v6 · libraryA replacement transaction did not raise fees enough to evict the pending transaction with the same nonce from the mempool.
Raise both maxFeePerGas and maxPriorityFeePerGas above the pending transaction's values by the node's price bump (geth defaults to 10%), not only one of them.
Official reference for REPLACEMENT_UNDERPRICED (opens in new tab)Code definition reviewedNONCE_EXPIREDethers v6 · libraryThe sending account has already used this nonce in a transaction that has been included.
Check whether an earlier attempt or another signer process already mined this nonce, or whether the nonce came from a lagging RPC node. Look up the mined transaction before resending.
Official reference for NONCE_EXPIRED (opens in new tab)Code definition reviewedTRANSACTION_REPLACEDethers v6 · libraryA pending transaction was replaced by another transaction with the same nonce; reason is repriced, cancelled, or replaced.
Track the replacement hash and its receipt. A repriced transaction may still have done the intended work; a cancelled or replaced one did not.
Official reference for TRANSACTION_REPLACED (opens in new tab)Code definition reviewed
THE PRINCIPLE
The nonce is the queue. You never fix a stuck transaction by sending a new one; you replace the one holding the slot.
FIRST MOVES
- Compare getTransactionCount at latest and pending: the gap is your queue, and the latest count is the slot that must be filled.
- If a nonce is missing entirely (a gap), nothing after it can mine until something is sent with that nonce.
- Speed up by resending the same nonce with both maxFeePerGas and maxPriorityFeePerGas raised; geth rejects replacements under a 10% bump on each.
- Cancel with a 0-value transfer to yourself at the same nonce and higher fees; the original may still win the race.
- Backend signers: allocate nonces from one place (a single sender or a database row lock), not from parallel workers reading pending counts.
- Watch for replacement when waiting on receipts and record the final hash against your own attempt ID.
TOOLS: THEN → NOW
PATTERN SNAPSHOT
const stuck = await publicClient.getTransaction({ hash: stuckHash });
const fees = await publicClient.estimateFeesPerGas();
const bump = (x: bigint) => (x * 125n) / 100n;
const max = (a: bigint, b: bigint) => (a > b ? a : b);
// Same nonce, 0 value to self: cancels if it mines before the original.
await walletClient.sendTransaction({
account,
to: account.address,
value: 0n,
nonce: stuck.nonce,
maxFeePerGas: max(bump(stuck.maxFeePerGas!), fees.maxFeePerGas),
maxPriorityFeePerGas: max(bump(stuck.maxPriorityFeePerGas!), fees.maxPriorityFeePerGas),
});CLOSE THE AI. EXPLAIN THIS.
Why can't a later transaction with a higher fee overtake an earlier nonce from the same account?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
- go-ethereum legacypool: replacement requires both fee cap and tip to exceed the price bump (opens in new tab)Checked
- Geth command-line options (txpool.pricebump default 10) (opens in new tab)Checked
- viem: waitForTransactionReceipt replacement detection (opens in new tab)Checked
- viem: Nonce Manager (opens in new tab)Checked
Guide reviewed