The error 'replacement transaction underpriced' means your node rejected a second transaction because it used the same nonce as an existing pooled transaction but did not increase the fee enough to meet the client's minimum bump threshold. This is a fee problem, not a nonce problem. To resolve it, inspect the stuck transaction with eth_getTransactionByHash, check the pending nonce with eth_getTransactionCount, then either bump the fee (same nonce, higher maxFeePerGas and maxPriorityFeePerGas), cancel it (same nonce, 0-value self-transfer at bumped fee), or wait if the fee is already adequate. Always compute the replacement fee from current base fee and priority fee estimates rather than guessing.
What 'Replacement Transaction Underpriced' Actually Means
When you send a transaction with eth_sendRawTransaction, the node checks whether another transaction from the same sender with the same nonce already exists in its mempool. If one does, the node treats your new transaction as a replacement and applies a replacement policy. The error 'replacement transaction underpriced' is returned when the new transaction's fee does not exceed the existing transaction's fee by the client's required minimum bump percentage. This is a fee rejection, not a nonce rejection. The nonce is correct; the price is not aggressive enough.
The Ethereum JSON-RPC specification defines eth_sendRawTransaction as submitting a signed transaction to the network. It does not define replacement rules; those are client-specific. Geth, for example, documents a default price bump threshold (commonly 10% for both tip and fee cap), but this value is configurable and varies by client and version. Always treat the exact threshold as 'documented / varies by client' and verify against your node's configuration or documentation. For reference, see the Geth transaction pool documentation.
Because the replacement is rejected, your original transaction remains in the pool. If that original transaction is stuck due to a low fee, you are now in a loop: you need to replace it, but your replacement must be priced high enough to both beat the old transaction and satisfy the client's bump rule.
- Same sender + same nonce = replacement attempt.
- Rejection reason: new fee does not exceed old fee by the client's minimum bump.
- The original transaction stays in the mempool; nothing is cancelled automatically.
Diagnostic Sequence: Confirm the Transaction Is Stuck and Read Its Fee Parameters
Before replacing anything, confirm the transaction is genuinely stuck and not already mined. Use eth_getTransactionByHash with the transaction hash. If the result is null, the transaction may have been dropped from the mempool entirely. If the result contains a blockNumber, the transaction is already mined and no replacement is needed. If blockNumber is null and the transaction is present, it is pending in the pool. For reference, see the Ethereum JSON-RPC API documentation.
Next, read the account's pending nonce with eth_getTransactionCount(address, 'pending'). This tells you the next nonce the network expects from this account, including pending transactions. Compare it to the stuck transaction's nonce. If the pending nonce is greater than the stuck nonce, there may be a gap or a queue of transactions behind the stuck one. If the pending nonce equals the stuck nonce, the stuck transaction is the head of the queue and must be resolved first.
Also read the stuck transaction's maxFeePerGas and maxPriorityFeePerGas (for EIP-1559 transactions) or gasPrice (for legacy transactions). These values are your baseline. Your replacement must exceed them by the client's bump threshold and also be competitive with the current market. For a deeper dive into fee estimation, see eth_feeHistory and fee estimation.
- eth_getTransactionByHash: check blockNumber (null = pending, non-null = mined).
- eth_getTransactionCount(address, 'pending'): get next usable nonce.
- Read maxFeePerGas and maxPriorityFeePerGas from the stuck transaction.
Why Transactions Get Stuck: Fee Market Dynamics and EIP-1559 Caps
A transaction becomes stuck when its fee is below what the market is willing to pay for inclusion. Under EIP-1559, a transaction specifies a maxFeePerGas and a maxPriorityFeePerGas. The effective fee paid is min(maxFeePerGas, baseFee + maxPriorityFeePerGas). If the base fee rises above your maxFeePerGas minus priority fee, your transaction becomes unminable because the protocol will not let you overpay beyond your cap. It sits in the pool until the base fee drops or you replace it. For reference, see the EIP-1559 specification.
Another scenario is a dropped transaction. If the mempool is full or the transaction has been evicted due to low fee, eth_getTransactionByHash may return null. In that case, the nonce is still free, and you can simply send a new transaction with the same nonce and a higher fee. This is not a replacement in the strict sense because there is nothing to replace, but the nonce management is identical.
Understanding the mempool helps you decide whether to wait or replace. The Reading the Ethereum mempool guide explains how to inspect pool contents and pending transactions.
- Stuck: fee below market clearing price or maxFeePerGas too low for current base fee.
- Dropped: transaction evicted; nonce is free again.
- EIP-1559 cap: effective fee cannot exceed maxFeePerGas.
Three Recovery Paths: Bump, Cancel, or Wait
The correct action depends on whether the transaction is still in the pool, whether the nonce is still free, and whether the current fee market justifies waiting. Use the decision table below to map your situation to the right path.
BUMP: If the transaction is still pending and you want it to execute, re-send the same nonce with a materially higher maxPriorityFeePerGas and maxFeePerGas. The new fee must exceed the old fee by the client's bump threshold and also be competitive with the current market. This is the most common fix.
CANCEL: If you no longer want the transaction to execute, re-send the same nonce with a 0-value self-transfer (to your own address) at the same bumped fee. This replacement will land and consume the nonce, effectively cancelling the original. The original transaction will be dropped from the pool once the replacement is mined.
WAIT: If the transaction's fee is genuinely adequate for current conditions and the pool is simply deep, waiting may be the best option. Replacing with an even higher fee could overpay unnecessarily. Monitor the base fee and your transaction's position in the pool.
- Still in pool, nonce free: BUMP or CANCEL with same nonce.
- Gone from pool, nonce free: send new transaction with same nonce and higher fee.
- Nonce consumed: transaction already mined; no action needed.
- Nonce still free but transaction pending: replacement required.
Decision Table: Symptom to Action
Use this table to quickly determine the correct recovery path. The key inputs are the result of eth_getTransactionByHash (present or null) and the comparison between the stuck transaction's nonce and the pending nonce from eth_getTransactionCount.
If the transaction is present and its nonce equals the pending nonce, it is the head of the queue. You must replace it to unblock subsequent transactions. If its nonce is less than the pending nonce, there may be other transactions ahead of it, but replacing it is still valid if you use the same nonce.
If the transaction is null and the pending nonce equals the stuck nonce, the nonce is free. You can send a new transaction with that nonce and a higher fee. If the pending nonce is greater than the stuck nonce, the nonce has been consumed by another transaction, and the stuck transaction is irrelevant.
- Present + nonce == pending nonce: replace with same nonce, higher fee.
- Present + nonce < pending nonce: replace with same nonce, higher fee; check for gaps.
- Null + nonce == pending nonce: send new transaction with same nonce, higher fee.
- Null + nonce < pending nonce: nonce already consumed; no action.
Computing a Valid Replacement Fee
Do not guess the replacement fee. Read the current base fee and a priority fee estimate from the network. You can use eth_feeHistory or eth_maxPriorityFeePerGas. Then set your replacement's maxPriorityFeePerGas and maxFeePerGas strictly above both the old transaction's values (by at least the client's bump threshold) and the current market estimates.
For EIP-1559, a safe formula is: newMaxPriorityFeePerGas = max(oldMaxPriorityFeePerGas * (1 + bump), currentPriorityFeeEstimate). newMaxFeePerGas = max(oldMaxFeePerGas * (1 + bump), currentBaseFee * 2 + newMaxPriorityFeePerGas). The bump factor is client-specific; a common default is 10%, but verify with your node. Always round up to avoid falling just below the threshold.
If you are using a legacy gasPrice transaction, the same logic applies: newGasPrice = max(oldGasPrice * (1 + bump), currentGasPriceEstimate).
- Read current base fee and priority fee estimate.
- Apply bump factor to old fee values.
- Take the maximum of bumped old fee and current market fee.
Runnable Node.js Example: Inspect, Bump, and Submit
This example uses ethers.js v6 to inspect a stuck transaction, compute a replacement fee, and submit a bump with the same nonce. It assumes you have the stuck transaction hash and the sender's private key. Replace the RPC URL with your provider endpoint. For reliable endpoints, see the RPC endpoints guide (RPC Assistant).
The code first fetches the stuck transaction and the pending nonce. It then calculates new fee values based on the old transaction's fees and the current network conditions. Finally, it sends a new transaction with the same nonce and the bumped fees. If the replacement is rejected with 'replacement transaction underpriced', increase the bump factor and retry.
const { ethers } = require('ethers');
async function bumpStuckTransaction(rpcUrl, privateKey, stuckTxHash) {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(privateKey, provider);
// 1. Fetch the stuck transaction
const stuckTx = await provider.getTransaction(stuckTxHash);
if (!stuckTx) {
console.log('Transaction not found in mempool. It may be dropped or mined.');
return;
}
if (stuckTx.blockNumber) {
console.log('Transaction already mined in block', stuckTx.blockNumber);
return;
}
// 2. Get pending nonce
const pendingNonce = await provider.getTransactionCount(wallet.address, 'pending');
console.log('Stuck nonce:', stuckTx.nonce, 'Pending nonce:', pendingNonce);
// 3. Compute replacement fees
const feeData = await provider.getFeeData();
const bumpFactor = 1.2; // 20% bump; adjust based on client threshold
const oldMaxPriority = stuckTx.maxPriorityFeePerGas || 0n;
const oldMaxFee = stuckTx.maxFeePerGas || stuckTx.gasPrice || 0n;
const newMaxPriority = oldMaxPriority * BigInt(Math.floor(bumpFactor * 100)) / 100n;
const newMaxFee = oldMaxFee * BigInt(Math.floor(bumpFactor * 100)) / 100n;
const finalMaxPriority = newMaxPriority > (feeData.maxPriorityFeePerGas || 0n) ? newMaxPriority : feeData.maxPriorityFeePerGas;
const finalMaxFee = newMaxFee > (feeData.maxFeePerGas || 0n) ? newMaxFee : feeData.maxFeePerGas;
console.log('Replacement fees:', { maxPriorityFeePerGas: finalMaxPriority, maxFeePerGas: finalMaxFee });
// 4. Send replacement with same nonce
const tx = await wallet.sendTransaction({
to: stuckTx.to,
value: stuckTx.value,
data: stuckTx.data,
nonce: stuckTx.nonce,
maxPriorityFeePerGas: finalMaxPriority,
maxFeePerGas: finalMaxFee,
gasLimit: stuckTx.gasLimit,
chainId: (await provider.getNetwork()).chainId
});
console.log('Replacement sent:', tx.hash);
await tx.wait();
console.log('Replacement mined');
}
// Usage:
// bumpStuckTransaction('https://your-rpc-endpoint', '0x...', '0x...');Handling the 'Replacement Underpriced' Error in a Retry Loop
If your replacement is rejected with 'replacement transaction underpriced', it means your bump was insufficient. The node's required bump threshold is higher than your increase, or the current market fee is higher than your new fee. In that case, increase the bump factor and retry. A common pattern is to start with a 10% bump, then 20%, then 50%, until the replacement is accepted.
Be aware that each retry must use the same nonce. If you accidentally use a new nonce, you create a gap in the nonce sequence, and all subsequent transactions will be stuck behind the gap. This is a common pitfall. Always verify the nonce before sending.
If you are using ethers.js, note that the library may automatically populate fee fields if you do not specify them. When replacing, always explicitly set maxFeePerGas and maxPriorityFeePerGas to your computed values to avoid the library using stale estimates.
- Retry with larger bump increments until accepted.
- Always use the same nonce for replacements.
- Explicitly set fee fields to avoid stale estimates.
Pitfalls and Network-Specific Considerations
Using a new nonce instead of the same one is the most damaging mistake. It creates a nonce gap, and the network will not mine any transaction with a higher nonce until the gap is filled. This can result in a queue of stuck transactions. For a detailed explanation of nonce management under concurrency, see EVM nonce management under concurrency.
Bumping without refreshing the base fee is another pitfall. If the base fee has risen since you first sent the transaction, your bumped maxFeePerGas may still be below the current base fee plus priority fee, making the replacement unminable. Always fetch fresh fee data before computing the replacement.
On some networks, the fee model differs. Layer 2 sequencers may have different replacement semantics, and some chains use a legacy gasPrice model without EIP-1559. On BSC-style chains, the gasPrice is the only fee parameter. Always verify the replacement rules for the specific network. For Ethereum mainnet, the Ethereum network page provides an overview of supported RPC methods.
Private mempools and MEV relays offer an alternative route. Sending a transaction through a private relay can bypass the public mempool and avoid replacement rules, but this is a specialized workflow and not a general fix.
- New nonce creates a gap and blocks successors.
- Stale base fee can make a bumped transaction unminable.
- L2 and non-EIP-1559 chains have different replacement rules.
- Private mempools bypass public pool replacement policies.
Limitations: Reorgs, Nonce Gaps, and Sequencer Differences
Reorganizations can complicate replacement. If a replacement is mined and then a reorg occurs, the original transaction may reappear in the pool. In that case, you may need to replace again. Always monitor the transaction status after a replacement.
Nonce gaps are a persistent risk. If you have a gap, no transaction with a higher nonce will be mined until the gap is filled. You must either send a transaction with the missing nonce or replace the transaction that created the gap. Tools like eth_getTransactionCount with 'pending' help identify gaps.
On L2s and other sequencer-based networks, replacement semantics may differ. Some sequencers process transactions in FIFO order and may not support fee-based replacement at all. Always check the network's documentation. For general RPC reliability, see Ethereum RPC timeout handling.
- Reorgs can resurrect replaced transactions.
- Nonce gaps block all higher-nonce transactions.
- L2 sequencers may not support fee-based replacement.
Next Steps: Monitoring, Automation, and Provider Selection
After resolving a stuck transaction, consider setting up monitoring to alert you when transactions remain pending beyond a threshold. This can help you act before the fee market moves too far. You can use eth_getTransactionByHash in a polling loop or subscribe to pending transactions via WebSocket.
For production systems, automate the bump logic with a retry loop that increases the fee until the replacement is accepted. Always cap the maximum fee to avoid overpaying. Test your logic on a testnet before deploying to mainnet.
Choosing a reliable RPC provider is critical for timely transaction submission and replacement. OnFinality offers API service with endpoints for Ethereum and other networks. For pricing details, see RPC pricing. To explore more troubleshooting guides, visit the OnFinality Learn hub.
- Monitor pending transactions with polling or WebSocket.
- Automate bump logic with a capped retry loop.
- Use a reliable RPC provider for consistent submission.