Breaking the Single-Wallet Nonce Ceiling: An Account-Pool Architecture for High-Throughput EVM Transaction Submission

Abstract. Every account on an Ethereum Virtual Machine (EVM) chain submits transactions through a strictly sequential counter — the nonce. This single, ordered counter is both a correctness guarantee and a throughput ceiling: one account can only land one transaction at a time, and a single stuck transaction blocks every transaction queued behind it (head-of-line blocking). This article presents walletsforce, a general-purpose EVM account pool that treats a set of signer accounts as N independent nonce lanes, breaking the single-account ceiling and confining head-of-line blocking to a single lane. We describe the problem formally, derive the system's three-part decomposition (Pool, Supervisor, Substrate), and analyse the mechanisms that make concurrent submission correct: a per-account asynchronous mutex over nonce allocation, replacement of stuck transactions by re-signing the same nonce with bumped fees, idempotency-key correlation across changing transaction hashes, and write-ahead durability for crash recovery. We conclude with the ownership contract that partitions responsibility between the library and its caller, and the two deployment topologies the design admits.

1. Introduction

Services that must issue a high volume of on-chain transactions — indexers writing checkpoints, bridges relaying messages, game backends minting assets, market makers, airdrop distributors — quickly collide with a property of the EVM account model that is easy to state and hard to engineer around: the nonce. A nonce is a per-account counter that increments by exactly one per included transaction. The chain will not include the transaction at nonce k+1 until the transaction at nonce k has been included. Consequently a single externally owned account (EOA) is a strictly serial transaction pipeline.

This serialisation has two costs. First, a throughput ceiling: the maximum rate at which one account can land transactions is bounded by the round-trip latency of getting each nonce mined before the next can be. Second, head-of-line (HOL) blocking: if the transaction at nonce k becomes stuck — underpriced relative to a rising base fee, or dropped from the mempool — every later transaction from that account is blocked behind it, regardless of how urgent they are.

Doing this correctly by hand is notoriously error-prone. The operator must track the next nonce without gaps or collisions, price gas, detect stuck transactions, re-sign them at a higher fee against the same nonce, avoid double-spending an already-mined nonce, and recover in-flight work if the process crashes mid-send. walletsforce reframes the problem: rather than optimise one account, it manages a pool of owned accounts, where each account is one independent nonce lane. With N accounts, up to N transactions are in flight simultaneously, the aggregate ceiling scales with N, and a stuck transaction blocks only its own lane. All the bookkeeping is hidden behind a single call, submit(tx).

2. Background: the nonce as a serialising primitive

Formally, let an account a have an on-chain nonce n(a) equal to the number of transactions already included from a. A transaction from a is valid for inclusion only if its nonce equals n(a); a transaction with nonce n(a)+j for j>0 is a queued (future) transaction that the node holds but cannot mine until the intervening nonces are filled. This yields three failure modes an unmanaged sender must handle:

  • Gaps. Allocating nonce k+2 while k+1 is never broadcast leaves a permanent gap that stalls the account until k+1 is filled.
  • Collisions. Two concurrent code paths both reading "the next nonce" and both broadcasting at that value; only one can win, the other is rejected.
  • Stuck head-of-line. A validly numbered but underpriced transaction at nonce k that never mines, freezing the lane.

The correctness requirement is therefore a single writer per account that allocates strictly increasing, gap-free nonces and advances its cursor only when a broadcast actually succeeds. The scalability requirement is to have many such single-writer lanes running concurrently. walletsforce satisfies both by construction.

3. System design

The system decomposes into three parts that are wired together and share one runtime:

  • WalletForcePool — the submit surface. Reactive, with no background loop. For each request it performs the pipeline route → allocate nonce → estimate gas → price fees → sign → broadcast, and exposes the event API (on, waitForConfirmation).
  • Supervisor — the reconcile loop. On a timer it polls receipts, confirms or replaces stuck transactions, refreshes account balances, and (given a funder) refills drained accounts. It is started explicitly and must run in exactly one place per account set.
  • Substrate — the shared runtime. A bundle { cache, store, bus, accounts } built once by createSubstrate(config) and injected into both the Pool and the Supervisor. Sharing one substrate is what allows a submit on one side to meet its confirmation on the other over a single working set.

The separation is deliberate: the Pool is the fast, synchronous-feeling request path; the Supervisor is the slow, periodic truth-reconciliation path; and they communicate only through the Substrate's cache, durable store, and event bus. The ManagedAccount — one object per address — is the unit of parallelism: it owns its send pipeline, its confirm/replace step, and its in-flight cap.

// Build the shared substrate ONCE, inject into both.
const substrate  = createSubstrate(config);
const pool       = new WalletForcePool(config, substrate);
const supervisor = new Supervisor({ ...config, confirmTickMs: 4_000 }, substrate);

await pool.start();   // boot reconcile: rebuild in-flight state from the store
supervisor.start();   // start the confirm / replace / refresh loop

const { account, nonce, hash } = await pool.submit(
  { to: contractAddress, data: calldata },
  { idempotencyKey: "packet-14361" },   // resolves on BROADCAST, not confirmation
);
const rec = await pool.waitForConfirmation("packet-14361", { timeoutMs: 120_000 });

4. Correctness mechanisms

4.1 The nonce lane: an asynchronous per-account mutex

Each account's nonce allocation is serialised by an asynchronous mutex exposed as PoolCache.withNonce(account, fn). The critical section covers allocation and broadcast together, and — crucially — the cursor advances only if fn resolves. If signing or broadcast throws (e.g. the in-flight cap is hit or the RPC rejects), the cursor does not move, so no gap is created and no nonce is burned. In the default single-process deployment this is an in-process mutex; in the shared deployment it becomes a per-account distributed lock, serialising the same lane across pods.

4.2 Routing: choosing the lane

A Router selects which account handles each request among the healthy candidates. Two policies coexist. Unordered requests are routed by a WalletSelector, whose default picks the least-in-flight account to balance load. Requests carrying an orderingKey are routed stickily: all requests sharing a key are pinned to the same account (ref-counted, evicted at zero references) so that they land in submission order on one lane. This gives callers per-key ordering when they need it, and maximal parallelism when they do not.

4.3 Fee pricing and stuck-transaction replacement

Gas pricing and the replacement-bump policy are isolated behind a FeeOracle seam, with bundled implementations for legacy and EIP-1559 fee markets. When the Supervisor observes a transaction that has been unmined for longer than stuckAfterMs, it re-prices via bump(previousFees) and re-signs the same nonce at a higher fee — a replacement must beat the node's replacement rule (approximately a 12.5% increase), which is the library's default bump. This is the key subtlety of the design: a replacement changes the transaction's hash while preserving its nonce and its logical identity. After maxAttempts replacements a transaction is declared failed.

4.4 Confirmation and idempotency-key correlation

Because replacement changes the hash, the hash is not a stable identifier for a logical transaction. walletsforce therefore correlates on a caller-supplied idempotencyKey, which is echoed on every lifecycle event and is the argument to waitForConfirmation(key). The submit() call resolves early — on broadcast into the mempool — while waitForConfirmation() resolves later on confirmed (or rejects on reverted/failed). The persisted status set (pending | broadcast | mined | confirmed | reverted | failed) is deliberately distinct from the observable event set (broadcast | mined | confirmed | replaced | reverted | failed): pending is an internal write-ahead status that is never emitted, and replaced is an emit-only fee-bump notification that is never stored as a row status.

4.5 Durability and crash recovery

Durability is a deliberate choice, expressed by which PoolStore is injected. With the default in-memory store nothing survives a crash. Inject the bundled SQLite store (or a custom PoolStore) and the Pool write-aheads the transaction row with status pending before broadcast; on restart, pool.start() calls restore(), which reloads non-terminal rows, reads the current on-chain nonce, seeds the cursor (advance-only), and re-tracks in-flight transactions so the Supervisor's next tick resumes them. The persistence model is two tables — accounts (one row per owned account) and transactions (one row per logical transaction across its whole lifecycle, keyed by idempotencyKey and retaining the full signable payload so a stuck transaction can be re-signed even after a crash).

5. Deployment topologies

The Pool/Supervisor/Substrate split yields exactly two shapes:

ModeCache / Store / BusTopology
individual (default)in-memoryone process runs Pool and Supervisor
groupshared (Redis cache/bus, SQL store)many submit pods run Pools; one pod runs the Supervisor

The invariant across both is that the Supervisor's reconcile tick must run in exactly one place per account set — it must never execute concurrently across pods. In group mode the nonce lane's per-account lock is what preserves single-writer correctness even though many pods submit.

6. The ownership contract

A recurring theme is an explicit partition of responsibility. The library makes a bounded set of guarantees and pushes the rest — deliberately — to the caller, whose business semantics it cannot know:

walletsforce guaranteesthe caller owns
correct, gap-free nonce per accountidempotency / dedupe on the business unit
single-writer per account (static partition)exactly-once effect across a crash
no in-process duplicate sendschoosing durability (in-memory vs a durable store)
receipt tracking + stuck-tx replacementrunning the Supervisor in exactly one place (group mode)

The distinction between no duplicate sends (which the library guarantees within a process) and exactly-once effect (which only the caller can guarantee, by checking its own idempotency key before calling submit) is the crux of using the system safely.

7. Extensibility: three seams

The engine's node-, custody-, and pricing-specific behaviour is isolated behind three narrow interfaces, so the core scheduling logic is testable and portable:

  • Signer (custody) — signs a fully specified transaction and returns raw bytes. Bundled: an in-process key signer and a BIP-44 HD-wallet signer that derives N lanes from a single mnemonic; production deployments implement it over a KMS/HSM.
  • FeeOracle (pricing) — estimates initial fees and computes replacement bumps. Bundled: legacy and EIP-1559 oracles.
  • ChainClient (node I/O) — the single chokepoint for reads, broadcast, and RPC error classification. Bundled: an implementation over viem.

An optional Funder seam (bundled as TreasuryFunder) auto-refills drained accounts from a separate treasury account, so a long-running pool does not silently degrade as lanes run out of gas.

8. Discussion and limitations

The design's central trade-off is that horizontal nonce parallelism requires owning multiple funded accounts: throughput scales with N, but so does the operational surface (funding, key custody, monitoring). Per-key ordering is available but is a local guarantee within a single sticky lane, not a global total order across the pool. As documented by the authors, the group-mode substrate is interface-complete but the Redis cache/bus, SQL store, and Supervisor leader-lease implementations are described as remaining work; readers should treat single-process (individual) mode as the presently exercised path. Finally, the library is ESM-only and pins viem as a peer dependency, and the SQLite durable store depends on Node's built-in node:sqlite (Node ≥ 22.5).

9. Conclusion

walletsforce is a compact but carefully layered answer to a specific structural constraint of EVM chains. By elevating the account from an implementation detail to the unit of parallelism — one account, one nonce lane — it converts a serial, HOL-blocking bottleneck into a horizontally scalable pool, while a small number of invariants (single writer per lane, advance-cursor-only-on-success, hash-independent identity via idempotency keys, and optional write-ahead durability) keep concurrent submission correct. Its clean split of a reactive submit surface, a periodic reconcile loop, and a shared substrate is what lets the same code run both as a single embedded process and as a multi-pod fleet.


References & further reading

  • walletsforce — source repository and README.md / ARCHITECTURE.md: github.com/taintpro98/walletsforce (MIT-licensed; version 0.3.1 at time of writing).
  • EIP-1559 — Fee market change for ETH 1.0 chains (base fee + priority fee model).
  • EIP-155 — Simple replay attack protection (chain-id in the signable transaction).
  • BIP-44 — Multi-account hierarchy for deterministic wallets (HD derivation of N lanes from one seed).
  • viem — the TypeScript EVM client used by the bundled ChainClient.

This post describes the system as implemented in the repository; it reports the design and the authors' stated status rather than independent benchmarks. No performance figures are claimed.