Where does it stop?
The node accepted the transaction and returned a hash. It is in the mempool, but no block has included it. Nothing failed yet, so there is no error from the node, only a wait that doesn’t end.
Why is it stuck?
There are two common reasons. Check them in this order.
1. A nonce gap. An account’s transactions run in nonce order. If nonce 0 was never sent, nonce 1 can’t run. geth calls it “queued”, scheduled for future execution, as opposed to “pending” for the next blocks.
2. The fee is below the base fee. EIP-1559 only lets a block include a transaction whose maxFeePerGas is at least that block’s base fee. If the base fee climbed after you sent, your transaction waits for it to come down. A cap that is already below the base fee when you send can be refused outright: that is max fee per gas less than block base fee.
How do I find which one it is?
- Compare nonces. Read the account’s nonce with
blockTag: 'latest'and look up your transaction’s nonce. If yours is higher, the nonces in between are missing. - Compare fees. Look up the latest block’s
baseFeePerGas. If yourmaxFeePerGasis below it, the fee is the problem. - Check where the node keeps it. On a node that exposes
txpool_status, a gap shows as queued, not pending.
import type { Hash, PublicClient } from 'viem';
declare const client: PublicClient;declare const hash: Hash; // the stuck transaction
const tx = await client.getTransaction({ hash });const latest = await client.getTransactionCount({ address: tx.from, blockTag: 'latest' });const block = await client.getBlock();
if (tx.nonce > latest) console.log(`nonce gap: ${latest} to ${tx.nonce - 1} are missing`);if (tx.maxFeePerGas !== undefined && block.baseFeePerGas !== null && tx.maxFeePerGas < block.baseFeePerGas) console.log('max fee is below the current base fee');How do I fix it?
- Nonce gap: send the missing nonce. In our run, both transactions were mined in the next block.
- Fee too low: replace it. Send the same nonce with both fee fields at least 10% higher, geth’s default bump. To give up on it, send 0 to yourself at the same nonce instead.
- In code: set a
timeouton the wait and decide what to do when it fires. viem’s default is 180 seconds.
If you replace it, the old hash shows as dropped and replaced on a block explorer.
Why does the gap happen?
- A send failed after the nonce was taken. Code that counts nonces itself moved on while the node never got the transaction.
- Two senders share one key. Each keeps its own count, and one skips ahead.
- A transaction was dropped. A node evicted it, and the next nonce now waits for it.
What does it look like in a trace?
The send span ends normally with a hash. The confirm span is where the wait shows:
- error.type
- timeout
- blockchain.tx.status
- (not set)
A long gap between send and confirm, or a timeout, is the trace’s sign of a stuck transaction. The steps from hash to receipt are in the transaction lifecycle.