Where does it stop?
The node checks every transaction before it puts it in the mempool, and the nonce is one of the checks. When it fails, the node answers at once with an error. There is no transaction hash and nothing reaches a block.
What is a nonce?
A nonce is a counter on the account: the number of transactions it has sent. Only one transaction with a given nonce can run for an account, and that is what stops a signed transaction from being replayed.
So the next nonce is always the number of transactions the account has sent so far. Anything lower was used already, and the node says nonce too low.
Why does it happen?
Three causes cover most cases:
- You set the nonce yourself and reused one. A retry copies the nonce of the first attempt, and the first attempt was mined.
- Two senders share one account. Two workers, or two agent tool calls, each keep their own count. Sooner or later both send the same number.
- A retry after a timeout. The first attempt may have reached the chain even though no answer came back. If it was mined, sending the nonce again gives this error. If it is still waiting, you get replacement transaction underpriced instead.
How do I fix it?
- Do not pass
nonceunless you must. With a local account (a private key), viem asks the node for the account’s transaction count at thependingblock and uses that. With an address-only account, such as a wallet or a dev node that signs for you, the wallet or node picks the nonce. - Give the account a nonce manager when sends can overlap. With a local account, viem can keep the counter in your process, so concurrent sends get consecutive nonces:
import { createWalletClient, http, nonceManager } from 'viem';import { privateKeyToAccount } from 'viem/accounts';import { mainnet } from 'viem/chains';
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`, { nonceManager });const client = createWalletClient({ account, chain: mainnet, transport: http() });
// Concurrent sends from this process get consecutive nonces.await Promise.all([ client.sendTransaction({ to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', value: 1n }), client.sendTransaction({ to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', value: 2n }),]);- Before you retry after a timeout, find out what happened. Look up the first attempt by its hash, or read the account’s count again, and only send again if it did not land. A nonce manager covers one process: if several processes send from the same account, give each its own account or send through one place.
What does it look like in a trace?
With hashspan, the refused send is a send span that ends with an error and has no transaction hash, since the node never named one:
- error.type
- NonceTooLowError
- exception.message
- Nonce provided for the transaction is lower than the current nonce of the account.
- blockchain.tx.hash
- not set
By default a refused send records the error type, and since hashspan 1.1.0 that is the name viem gives the refusal: NonceTooLowError, FeeCapTooLowError, InsufficientFundsError or IntrinsicGasTooLowError. Where viem has no name for it, as with a replacement refusal, it is TransactionRejectedRpcError. Before 1.1.0 every one of them was recorded as TransactionExecutionError, the wrapper viem puts around them. The exception event still carries that wrapper as exception.type. To read the node’s wording as well, turn on errorMessages: 'sanitized' (see the core options). More on how a failed span is marked: span status.