How do I fix "nonce too low" and "replacement transaction underpriced" errors? · Jumpstart Blockchain
How do I fix "nonce too low" and "replacement transaction underpriced" errors?
Nonce too low means that nonce is already used; underpriced means a pending tx holds it and you didn't raise both fee fields enough. Pick nonces from "pending" and serialize sends per key.
Error: nonce too low
Error: replacement transaction underpriced
These two errors show up together because they come from the same rule: every Ethereum account sends transactions in strict nonce order, and each nonce can be used exactly once. Fixing them means knowing which nonce your code is using and why.
How nonces work
Every externally owned account has a counter. Your first transaction uses nonce 0, the next 1, and so on. A node will only include your transaction if its nonce equals the account's current on-chain count. That gives you three cases:
Your nonce vs. on-chain count
What happens
Lower
Rejected with nonce too low. That nonce is already used by a mined transaction.
Equal
Eligible for the next block.
Higher
Accepted into the mempool but held as "queued" until the gap is filled.
The mempool adds one more rule. If a transaction with the same sender and nonce is already pending, a new one with that nonce is treated as a replacement. Nodes only accept it if it pays meaningfully more. Geth's default price bump is 10%, and for EIP-1559 transactions both maxFeePerGas and maxPriorityFeePerGas must go up by at least that much. Other clients and RPC providers can require more. If yours doesn't, you get replacement transaction underpriced. If you resend the exact same signed transaction, you'll usually see already known instead.
Why "nonce too low" happens
You asked for the wrong count.getTransactionCount(address, "latest") counts only mined transactions. If you have a transaction pending, the next free nonce is higher. Use "pending" when picking a nonce for a new send.
Concurrent sends. Two async tasks, two worker processes or two servers sharing one key all fetch the same count and sign with the same nonce. One wins, the other fails.
You hard-coded or cached a nonce and the account has moved on.
Local chain reset. You restarted Hardhat or Anvil, so the chain is back at nonce 0, but MetaMask still remembers the old count. Clear the account's activity/nonce data in MetaMask's advanced settings, or switch networks and back.
MEV is profit from ordering transactions. A sandwich attack front-runs and back-runs a public swap, capped by its slippage. Use tight minimum outputs from fresh quotes and private RPCs.
Retry logic resent a transaction that actually got mined. The first attempt timed out on your side but landed on-chain.
Why "replacement transaction underpriced" happens
You're sending with a nonce that already has a pending transaction and your fees aren't high enough to replace it. Usually one of these:
a stuck transaction is still in the mempool and your code reused its nonce (often after fixing "nonce too low" by fetching the count with "pending" in one place and "latest" in another);
you tried to speed up or cancel a transaction but only raised one of the two fee fields, or raised them by less than the bump;
you bumped the fees but the original had a higher maxFeePerGas than your new estimate.
Diagnose first
Compare the mined count with the pending count:
import { ethers } from"ethers";
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const address = "0xYourAddress";
const latest = await provider.getTransactionCount(address, "latest");
const pending = await provider.getTransactionCount(address, "pending");
console.log({ latest, pending });
// latest === pending -> nothing pending, next nonce is `latest`// pending > latest -> nonces latest..pending-1 are waiting in the mempool
Keep in mind that "pending" reflects only the mempool of the node you're talking to. Load-balanced RPC endpoints can give different answers on consecutive calls.
Fix: replace a pending transaction properly
To replace a pending transaction, send a new one with the same nonce and fees at least ~10% higher on both fields. Take the higher of the bumped old fees and the current market fees, so the replacement is also good enough to be mined:
import { ethers, isError } from"ethers";
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const stuck = await provider.getTransaction("0xStuckTxHash");
if (!stuck || stuck.blockNumber !== null) thrownewError("not pending");
// Assumes an EIP-1559 (type 2) transaction; legacy ones use gasPrice instead.constbump = (x: bigint) => (x * 115n) / 100n; // +15%, above the 10% minimumconstmax = (a: bigint, b: bigint) => (a > b ? a : b);
const fee = await provider.getFeeData();
const maxPriorityFeePerGas = max(bump(stuck.maxPriorityFeePerGas!), fee.maxPriorityFeePerGas!);
const maxFeePerGas = max(bump(stuck.maxFeePerGas!), fee.maxFeePerGas!);
try {
const tx = await wallet.sendTransaction({
to: stuck.to,
data: stuck.data,
value: stuck.value,
nonce: stuck.nonce,
maxPriorityFeePerGas,
maxFeePerGas: max(maxFeePerGas, maxPriorityFeePerGas),
});
console.log("replacement sent:", tx.hash);
} catch (e) {
if (isError(e, "REPLACEMENT_UNDERPRICED")) console.log("bump the fees further");
elseif (isError(e, "NONCE_EXPIRED")) console.log("original was already mined");
elsethrow e;
}
The NONCE_EXPIRED branch matters: a replacement is a race, and if the original gets mined first your replacement fails with "nonce too low". Check the receipt of the original before assuming anything.
Fix: stop producing duplicate nonces
For a single process sending many transactions, let ethers track nonces locally with NonceManager:
import { ethers, NonceManager } from"ethers";
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const signer = newNonceManager(new ethers.Wallet(process.env.PRIVATE_KEY!, provider));
const recipients = ["0xRecipientA", "0xRecipientB", "0xRecipientC"];
// These can run concurrently; each gets the next nonce.const txs = awaitPromise.all(
recipients.map((to) => signer.sendTransaction({ to, value: ethers.parseEther("0.01") }))
);
NonceManager fetches the count once and increments in memory. If a send fails before broadcast, call signer.reset() so it re-reads the count from the node, otherwise you'll leave a gap that blocks every later transaction.
NonceManager only helps inside one process. If several workers or servers send from the same address, you need one of:
a single sender service that owns the key and serializes sends through a queue;
a shared nonce allocator (for example an atomic counter in Redis or a database row lock) that hands out nonces and reconciles against the chain on startup;
separate keys per worker, which avoids coordination entirely.
Checklist
Compare getTransactionCount(addr, "latest") with "pending" to see whether anything is stuck.
Pick new nonces from "pending", not "latest".
To replace, reuse the nonce and raise bothmaxFeePerGas and maxPriorityFeePerGas by more than 10%.
Handle NONCE_EXPIRED after a replacement: the original may have been mined.
Serialize sends per key with NonceManager, a queue or separate keys. Never let two processes guess the same nonce.
After resetting a local chain, reset the wallet's cached nonce too.
Approve is safe; unlimited, forgotten allowances and unread permit signatures are not. Approve exact amounts, use permit with short deadlines, read every typed-data signature, and revoke old approvals.
Storage dominates gas costs: pack variables, cache storage reads, use constant, immutable, calldata and custom errors, emit events for off-chain data, and measure every change with forge snapshot.