<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SwiftNodes</title>
    <description>The latest articles on DEV Community by SwiftNodes (@swiftnodes).</description>
    <link>https://dev.to/swiftnodes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023672%2F97c1b4d7-aef6-4fc3-b459-d4c40cfe7c8c.jpg</url>
      <title>DEV Community: SwiftNodes</title>
      <link>https://dev.to/swiftnodes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swiftnodes"/>
    <language>en</language>
    <item>
      <title>Nonce Management for High-Throughput Senders</title>
      <dc:creator>SwiftNodes</dc:creator>
      <pubDate>Fri, 10 Jul 2026 07:29:11 +0000</pubDate>
      <link>https://dev.to/swiftnodes/nonce-management-for-high-throughput-senders-29pb</link>
      <guid>https://dev.to/swiftnodes/nonce-management-for-high-throughput-senders-29pb</guid>
      <description>&lt;p&gt;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 &lt;em&gt;same&lt;/em&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a nonce actually is
&lt;/h2&gt;

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

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

&lt;h2&gt;
  
  
  The two nonce views a node gives you
&lt;/h2&gt;

&lt;p&gt;When you ask a node for an account's nonce via &lt;code&gt;eth_getTransactionCount&lt;/code&gt;, the answer depends on the block tag:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;Returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eth_getTransactionCount(addr, "latest")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Count of transactions &lt;strong&gt;mined&lt;/strong&gt; so far (confirmed nonce)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eth_getTransactionCount(addr, "pending")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Confirmed &lt;strong&gt;plus&lt;/strong&gt; what's sitting in &lt;em&gt;this node's&lt;/em&gt; mempool&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For low-volume sending, &lt;code&gt;"pending"&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;"pending"&lt;/code&gt; breaks under load
&lt;/h2&gt;

&lt;p&gt;Three things make &lt;code&gt;eth_getTransactionCount(addr, "pending")&lt;/code&gt; unreliable as your source of truth once you're sending fast:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It's a network round-trip.&lt;/strong&gt; If you fire 50 transactions in the time one &lt;code&gt;getTransactionCount&lt;/code&gt; call returns, several will read the &lt;em&gt;same&lt;/em&gt; "pending" value and collide on a nonce. Only one of each nonce survives; the rest are rejected as &lt;code&gt;nonce too low&lt;/code&gt; or silently replace each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The mempool view is per-node and eventually-consistent.&lt;/strong&gt; "Pending" reflects &lt;em&gt;that specific node's&lt;/em&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropped transactions create gaps.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix is the same pattern every high-throughput sender converges on: &lt;strong&gt;stop asking the node for each nonce. Track it yourself.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Manage the nonce locally
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Minimal local nonce manager (single account)&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;nextNonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Seed from confirmed state — "latest", not "pending"&lt;/span&gt;
  &lt;span class="nx"&gt;nextNonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTransactionCount&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;blockTag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;latest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;takeNonce&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextNonce&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;nextNonce&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// reserve it immediately, before the send returns&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Seed from &lt;strong&gt;&lt;code&gt;"latest"&lt;/code&gt;&lt;/strong&gt; (confirmed), not &lt;code&gt;"pending"&lt;/code&gt; — you're tracking your own in-flight transactions in memory, so you don't want the node's fuzzy pending view double-counting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling the things that go wrong
&lt;/h2&gt;

&lt;p&gt;A local counter is necessary but not sufficient — you also need to handle the failure cases, because at volume they &lt;em&gt;will&lt;/em&gt; happen:&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;takeNonce&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendRawTransaction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;nextNonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextNonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// give the nonce back&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A transaction gets stuck (underpriced).&lt;/strong&gt; It occupies its nonce in the mempool and blocks all higher nonces. You can't skip it — you have to &lt;strong&gt;replace&lt;/strong&gt; it: resend the &lt;em&gt;same nonce&lt;/em&gt; 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 &lt;a href="https://swiftnodes.io/blog/ethereum-mempool-explained" rel="noopener noreferrer"&gt;the mempool explainer&lt;/a&gt; — at scale you want it automated: if a nonce hasn't confirmed within N blocks, auto-bump it.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Going wider: parallel nonce lanes
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;multiple sending accounts&lt;/strong&gt;: 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.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirm, don't assume
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://swiftnodes.io/blog/reading-transaction-receipts-eth-gettransactionreceipt" rel="noopener noreferrer"&gt;receipt&lt;/a&gt; (&lt;code&gt;status: 0x1&lt;/code&gt;), and remember a transaction can be &lt;a href="https://swiftnodes.io/blog/handling-chain-reorgs-indexer" rel="noopener noreferrer"&gt;reorged back out&lt;/a&gt; after appearing mined. Your nonce manager's state should follow &lt;em&gt;confirmed&lt;/em&gt; reality, not "I sent it."&lt;/p&gt;

&lt;p&gt;And on fast chains, all of this matters more. On a ~400ms &lt;a href="https://swiftnodes.io/blog/sei-rpc-parallel-evm" rel="noopener noreferrer"&gt;Sei&lt;/a&gt; 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 &lt;a href="https://swiftnodes.io/blog/estimating-gas-eth-estimategas-eip-1559" rel="noopener noreferrer"&gt;gas estimation&lt;/a&gt; so your replacements clear the fee bump on the first try.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

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




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://swiftnodes.io/blog/nonce-management-high-throughput-senders" rel="noopener noreferrer"&gt;SwiftNodes blog&lt;/a&gt;. SwiftNodes provides flat-rate multi-chain RPC endpoints — HTTP + WebSocket, 75+ chains, no per-request metering to punish a busy relayer. &lt;a href="https://swiftnodes.io/" rel="noopener noreferrer"&gt;Grab a free key&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>tutorial</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
