# Nonce Management for High-Throughput Senders

Here's a failure mode that takes down backends: a service sends transactions from a single hot wallet, it relies on the node to tell it the next nonce, and under load two transactions grab the *same* nonce — or one underpriced transaction gets stuck and every transaction behind it queues forever. If you're building a relayer, a market maker, a bridge, or any service that sends from one account at volume, nonce management is the thing that will bite you. Let's get it right.

## What a nonce actually is

Every Ethereum account has a **nonce**: a counter of how many transactions it has sent, starting at 0. Each transaction from an account must carry the **next** sequential nonce, and the network includes them in **strict nonce order** — nonce 5 can't be mined before nonce 4, ever.

This ordering is a feature (it prevents replay and gives you deterministic sequencing), but it's also the trap: the account is a **single serialized lane**. One transaction stuck at the front blocks everything behind it, no matter how high those later transactions bid.

## The two nonce views a node gives you

When you ask a node for an account's nonce via `eth_getTransactionCount`, the answer depends on the block tag:

| Call | Returns |
| --- | --- |
| `eth_getTransactionCount(addr, "latest")` | Count of transactions **mined** so far (confirmed nonce) |
| `eth_getTransactionCount(addr, "pending")` | Confirmed **plus** what's sitting in *this node's* mempool |

For low-volume sending, `"pending"` is the convenient answer: "give me the next nonce including my in-flight transactions." And for a single sender doing one transaction at a time, it works fine. The problem is what happens at scale.

## Why `"pending"` breaks under load

Three things make `eth_getTransactionCount(addr, "pending")` unreliable as your source of truth once you're sending fast:

1.  **It's a network round-trip.** If you fire 50 transactions in the time one `getTransactionCount` call returns, several will read the *same* "pending" value and collide on a nonce. Only one of each nonce survives; the rest are rejected as `nonce too low` or silently replace each other.
    
2.  **The mempool view is per-node and eventually-consistent.** "Pending" reflects *that specific node's* mempool. Behind a load balancer, or right after a transaction is dropped, different nodes disagree about what's pending — so the nonce you get back flaps.
    
3.  **Dropped transactions create gaps.** If an in-flight transaction gets evicted (underpriced, timed out), the node's "pending" count can move backward, and you can hand out a nonce you already used or skip one.
    

The fix is the same pattern every high-throughput sender converges on: **stop asking the node for each nonce. Track it yourself.**

## Manage the nonce locally

Keep an in-memory counter per sending account. Seed it once from the chain, then increment it yourself for every transaction you dispatch — the node becomes a fallback, not the source of truth.

```js
// Minimal local nonce manager (single account)
let nextNonce = null;

async function init(client, address) {
  // Seed from confirmed state — "latest", not "pending"
  nextNonce = await client.getTransactionCount({ address, blockTag: "latest" });
}

function takeNonce() {
  const n = nextNonce;
  nextNonce += 1;          // reserve it immediately, before the send returns
  return n;
}
```

The critical detail: **increment the counter the instant you allocate a nonce**, synchronously, *before* the async send resolves. That's what prevents two concurrent sends from taking the same number. Serialize allocation (a lock/queue around `takeNonce`) so it's atomic.

Seed from `"latest"` (confirmed), not `"pending"` — you're tracking your own in-flight transactions in memory, so you don't want the node's fuzzy pending view double-counting them.

## Handling the things that go wrong

A local counter is necessary but not sufficient — you also need to handle the failure cases, because at volume they *will* happen:

**A send fails outright (RPC rejects it).** The nonce you reserved was never broadcast. You must **return it to the pool** (or reset your counter to it), or you'll leave a permanent gap that stalls everything after it.

```js
const nonce = takeNonce();
try {
  await client.sendRawTransaction({ ...tx, nonce });
} catch (e) {
  nextNonce = Math.min(nextNonce, nonce);   // give the nonce back
  throw e;
}
```

**A transaction gets stuck (underpriced).** It occupies its nonce in the mempool and blocks all higher nonces. You can't skip it — you have to **replace** it: resend the *same nonce* with a higher fee (Ethereum needs a ~10%+ bump to accept a replacement). Either speed it up (same transaction, higher tip) or cancel it (a 0-value self-send at that nonce). This is the same replace-by-nonce mechanic covered in [the mempool explainer](https://swiftnodes.io/blog/ethereum-mempool-explained) — at scale you want it automated: if a nonce hasn't confirmed within N blocks, auto-bump it.

**Your counter drifts from reality.** After a crash, restart, or a batch of failures, reconcile: compare your local `nextNonce` against `getTransactionCount("latest")` and `("pending")`. If confirmed has caught up to or passed your counter, resync. A periodic reconciliation loop keeps a long-running sender honest.

## Going wider: parallel nonce lanes

A single account is one serialized lane — there's a ceiling on how fast it can drain, because everything is strictly ordered. When you need more throughput than one account can push, the standard move is **multiple sending accounts**: shard your outbound transactions across N hot wallets, each with its own independent nonce sequence. Ten accounts = ten parallel lanes, and a stuck transaction on one lane doesn't block the other nine. Relayers and market makers do exactly this. (It also contains blast radius: one wedged nonce degrades 1/N of your throughput, not all of it.)

## Confirm, don't assume

Allocating and broadcasting a nonce isn't the finish line — the transaction still has to land. Track each sent nonce until you see it confirmed in a [receipt](https://swiftnodes.io/blog/reading-transaction-receipts-eth-gettransactionreceipt) (`status: 0x1`), and remember a transaction can be [reorged back out](https://swiftnodes.io/blog/handling-chain-reorgs-indexer) after appearing mined. Your nonce manager's state should follow *confirmed* reality, not "I sent it."

And on fast chains, all of this matters more. On a ~400ms [Sei](https://swiftnodes.io/blog/sei-rpc-parallel-evm) or a ~1s L2, nonces advance quickly and the window for collisions is tighter — local management stops being an optimization and becomes a requirement. Pair it with accurate [gas estimation](https://swiftnodes.io/blog/estimating-gas-eth-estimategas-eip-1559) so your replacements clear the fee bump on the first try.

## The short version

Nonces are a strict per-account sequence, so one account is a single serialized lane and one stuck transaction blocks everything behind it. `eth_getTransactionCount("pending")` is fine for casual sending but collides and flaps under load — so high-throughput senders **track the nonce locally**, incrementing a counter the instant they allocate (seeded from `"latest"`), return nonces on send failure, **replace** stuck transactions by re-sending the same nonce with a higher fee, reconcile against the chain after crashes, and **shard across multiple accounts** for parallel lanes. Confirm every nonce against a receipt, and tighten all of it on fast chains.

* * *

*Originally published on the* [*SwiftNodes blog*](https://swiftnodes.io/blog/nonce-management-high-throughput-senders)*. SwiftNodes provides flat-rate multi-chain RPC endpoints — HTTP + WebSocket, 75+ chains, no per-request metering to punish a busy relayer.* [*Grab a free key*](https://swiftnodes.io/)*.*
