Ethereum nonce management gets tricky when several workers send transactions from the same wallet. Each asks the node for the next nonce, then both prepare a different transaction with that value. One operation can replace or block another instead of simply running alongside it.
For this article, we’re dealing with an EVM externally owned account. Let’s give one dispatcher responsibility for its submission sequence, then handle restarts and replacements explicitly.
Scope the dispatcher by chain and sender
A nonce belongs to a sender on a specific chain. Use both in the dispatcher key. Don’t coordinate only by an account label that might refer to several networks.
Ethereum’s account documentation describes the transaction nonce. Your application still needs a policy for allocating it across workers and reconciling it with pending and confirmed activity. Ethereum account reference.
Keep signing access away from ordinary task workers. A worker can ask for a transfer. The dispatcher checks that request and records what it will sign and send.
Reserve the nonce durably
Use a database transaction to allocate the next local nonce and create a transaction-intent row. A unique constraint on (chain_id, sender, nonce) helps prevent two active local allocations from claiming the same slot.
The allocation should also have a stable operation ID. If the worker retries the same operation, return the existing reservation after checking that the intended action matches.
Don’t assume a local counter can ignore the chain. Reconcile it against confirmed nonce, pending observations and your own transaction records before resuming after a restart or external wallet activity.
Build the allocation rule
The example models one dispatcher receiving several asynchronous requests. It reserves each nonce before yielding, reuses an existing operation and keeps replacement attempts under that same operation.
import assert from 'node:assert/strict';
let nextNonce=7;
const operations=new Map();
function reserve(id,action){
const old=operations.get(id);
if(old){if(old.action!==action)throw new Error('Intent mismatch');return old;}
const row={id,action,nonce:nextNonce++,attempts:[]};operations.set(id,row);return row;
}
function addAttempt(id,hash){const op=operations.get(id);if(!op)throw new Error('Missing operation');op.attempts.push(hash);}
const rows=await Promise.all(['a','b','c'].map(async id=>reserve(id,'transfer')));
assert.deepEqual(rows.map(r=>r.nonce),[7,8,9]);
assert.equal(reserve('a','transfer').nonce,7);
assert.throws(()=>reserve('a','different-action'));
addAttempt('a','hash-original');addAttempt('a','hash-replacement');
assert.equal(operations.get('a').nonce,7);
assert.equal(operations.get('a').attempts.length,2);
console.log('PASS: local nonce reservation, operation reuse and replacement history');
These allocations are atomic only within this single process. A production multi-process allocator needs a shared transaction or locking mechanism. The example tests the mapping and replacement identity, not a distributed allocator.
Persist signed attempts before broadcasting
Store the operation ID, nonce, signed transaction bytes, transaction hash and attempt state before transmission. Protect signed payloads and signing material according to the application’s security needs.
If a broadcast response disappears, the stored bytes let you identify and reconcile the exact attempt. Generating a fresh transfer immediately can create a second business action.
Keep the signing boundary narrow and auditable. Never put private keys into model prompts, queue bodies or ordinary diagnostic logs.
Replace a stuck attempt deliberately
A replacement uses the same sender and nonce but a different signed transaction. Define which fields may change under the original approval. Raising the fee under a configured cap is different from changing the recipient or value.
Keep all attempt hashes linked to the same business operation. A replacement being accepted into a node’s pool doesn’t prove the earlier attempt can no longer be included elsewhere.
Replacement acceptance rules depend on client and provider policy. Don’t hard-code an assumed universal fee-bump percentage without checking the deployed path. The EIP-1559 fee fields also need to be handled correctly for applicable transaction types. EIP-1559 specification.
Find the transaction blocking the sequence
Later nonces may wait behind an earlier unconfirmed one. Track oldest pending age and the first unresolved nonce, not only the number of transactions in your local queue.
Check receipts and current account state before deciding an attempt is dropped. One provider not seeing a transaction isn’t sufficient proof that no network participant can include it.
If another application uses the same wallet, the dispatcher must detect that external activity. A dedicated sender per controlled workflow can reduce coordination complexity, but it doesn’t remove the need for reconciliation.
Keep business outcomes separate from nonce outcomes
A used nonce tells you that a transaction from that sender was included. It does not tell you whether the contract call you wanted actually succeeded.
Inspect the actual receipt, status and relevant state changes for the included attempt. An included reverted transaction consumes the nonce while leaving the intended business action incomplete.
If the operation needs a new transaction after a confirmed revert, treat that as a new deliberate attempt under the business policy. Don’t let a generic network retry silently recreate the action with changed parameters.
Test collisions and recovery
Allocate concurrently from separate processes against the production database design. Restart after reservation, after signing and after broadcast. Check that the same operation keeps its nonce and attempt history.
Then introduce external wallet activity, a stuck first nonce and a replacement. Verify that later jobs don’t assume success merely because the local counter moved forward.
Start by making one component responsible for (chain, sender) allocation and storing its decisions. Once nonce ownership is explicit, recovery becomes a reconciliation task rather than a race between workers asking for the same next number.
Run the example and adapt it
Save the code as 27-wallet-nonce-dispatcher.mjs and run it with Node.js 24 or later:
node 27-wallet-nonce-dispatcher.mjs
The code above includes assertions you can run locally. It tests the local decision logic with controlled inputs; it doesn’t connect to a live provider or establish production performance. Keep that boundary when adapting it to your application.

