← Back to the library
Web3Applied · 8 min

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 · library

A 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 reviewed
NONCE_EXPIREDethers v6 · library

The 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 reviewed
TRANSACTION_REPLACEDethers v6 · library

A 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

  1. Compare getTransactionCount at latest and pending: the gap is your queue, and the latest count is the slot that must be filled.
  2. If a nonce is missing entirely (a gap), nothing after it can mine until something is sent with that nonce.
  3. Speed up by resending the same nonce with both maxFeePerGas and maxPriorityFeePerGas raised; geth rejects replacements under a 10% bump on each.
  4. Cancel with a 0-value transfer to yourself at the same nonce and higher fees; the original may still win the race.
  5. Backend signers: allocate nonces from one place (a single sender or a database row lock), not from parallel workers reading pending counts.
  6. Watch for replacement when waiting on receipts and record the final hash against your own attempt ID.

TOOLS: THEN → NOW

Legacy gasPrice bumpsEIP-1559 fee bumps on both maxFeePerGas and maxPriorityFeePerGas
Reading the pending nonce in each workerOne nonce allocator per signer (viem nonceManager in a single process, a DB-backed queue across processes)
Polling a single hashReceipt waits with replacement detection (viem onReplaced, ethers TRANSACTION_REPLACED)

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

SOURCES

Guide reviewed